From f6103bf53be4c3e8201ed632770c17becbbf1418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 Mar 2013 17:51:39 -0300 Subject: [PATCH 001/890] Initial cherrypy support --- examples/cherrypy_example/__init__.py | 0 examples/cherrypy_example/models.py | 0 social/apps/cherrypy_app/__init__.py | 0 social/apps/cherrypy_app/models.py | 76 ++++++++++++++++++++++++++ social/apps/cherrypy_app/utils.py | 41 ++++++++++++++ social/apps/cherrypy_app/views.py | 14 +++++ social/strategies/cherrypy_strategy.py | 68 +++++++++++++++++++++++ 7 files changed, 199 insertions(+) create mode 100644 examples/cherrypy_example/__init__.py create mode 100644 examples/cherrypy_example/models.py create mode 100644 social/apps/cherrypy_app/__init__.py create mode 100644 social/apps/cherrypy_app/models.py create mode 100644 social/apps/cherrypy_app/utils.py create mode 100644 social/apps/cherrypy_app/views.py create mode 100644 social/strategies/cherrypy_strategy.py diff --git a/examples/cherrypy_example/__init__.py b/examples/cherrypy_example/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/cherrypy_example/models.py b/examples/cherrypy_example/models.py new file mode 100644 index 000000000..e69de29bb diff --git a/social/apps/cherrypy_app/__init__.py b/social/apps/cherrypy_app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/social/apps/cherrypy_app/models.py b/social/apps/cherrypy_app/models.py new file mode 100644 index 000000000..b23ed7e94 --- /dev/null +++ b/social/apps/cherrypy_app/models.py @@ -0,0 +1,76 @@ +"""Flask SQLAlchemy ORM models for Social Auth""" +import cherrypy + +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.schema import UniqueConstraint +from sqlalchemy.ext.declarative import declarative_base + +from social.utils import setting_name, module_member +from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ + SQLAlchemyAssociationMixin, \ + SQLAlchemyNonceMixin, \ + BaseSQLAlchemyStorage +from social.apps.flask_app.fields import JSONType + + +SocialBase = declarative_base() + +UID_LENGTH = cherrypy.config.get(setting_name('UID_LENGTH'), 255) +User = module_member(cherrypy.config[setting_name('USER_MODEL')]) + + +class CherryPySocialBase(SocialBase): + @classmethod + def _session(cls): + return cherrypy.request.app.config['db_session'] + + +class UserSocialAuth(SQLAlchemyUserMixin, CherryPySocialBase): + """Social Auth association model""" + __tablename__ = 'social_auth_usersocialauth' + __table_args__ = (UniqueConstraint('provider', 'uid'),) + id = Column(Integer, primary_key=True) + provider = Column(String(32)) + uid = Column(String(UID_LENGTH)) + extra_data = Column(JSONType) + user_id = Column(Integer, ForeignKey(User.id), + nullable=False, index=True) + user = relationship(User, backref='social_auth') + + @classmethod + def username_max_length(cls): + return User.__table__.columns.get('username').type.length + + @classmethod + def user_model(cls): + return User + + +class Nonce(SQLAlchemyNonceMixin, CherryPySocialBase): + """One use numbers""" + __tablename__ = 'social_auth_nonce' + __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) + id = Column(Integer, primary_key=True) + server_url = Column(String(255)) + timestamp = Column(Integer) + salt = Column(String(40)) + + +class Association(SQLAlchemyAssociationMixin, CherryPySocialBase): + """OpenId account association""" + __tablename__ = 'social_auth_association' + __table_args__ = (UniqueConstraint('server_url', 'handle'),) + id = Column(Integer, primary_key=True) + server_url = Column(String(255)) + handle = Column(String(255)) + secret = Column(String(255)) # base64 encoded + issued = Column(Integer) + lifetime = Column(Integer) + assoc_type = Column(String(64)) + + +class CherryPyStorage(BaseSQLAlchemyStorage): + user = UserSocialAuth + nonce = Nonce + association = Association diff --git a/social/apps/cherrypy_app/utils.py b/social/apps/cherrypy_app/utils.py new file mode 100644 index 000000000..c7a685ae6 --- /dev/null +++ b/social/apps/cherrypy_app/utils.py @@ -0,0 +1,41 @@ +import cherrypy + +from functools import wraps + +from social.utils import setting_name, module_member +from social.strategies.utils import get_strategy + + +DEFAULTS = { + 'STRATEGY': 'social.strategies.cherrypy_strategy.CherryPyStrategy', + 'STORAGE': 'social.apps.cherrypy_app.models.CherryPyStorage' +} + + +def get_helper(name, do_import=False): + config = cherrypy.request.app.config.get(setting_name(name), + DEFAULTS.get(name, None)) + return do_import and module_member(config) or config + + +def strategy(redirect_uri=None): + def decorator(func): + @wraps(func) + def wrapper(self, backend=None, *args, **kwargs): + uri = redirect_uri + + if uri and backend and '%(backend)s' in uri: + uri = uri % {'backend': backend} + + backends = get_helper('AUTHENTICATION_BACKENDS') + strategy = get_helper('STRATEGY') + storage = get_helper('STORAGE') + self.strategy = get_strategy(backends, strategy, storage, + cherrypy.request, backend, + redirect_uri=uri, *args, **kwargs) + if backend: + return func(self, backend=backend, *args, **kwargs) + else: + return func(self, *args, **kwargs) + return wrapper + return decorator diff --git a/social/apps/cherrypy_app/views.py b/social/apps/cherrypy_app/views.py new file mode 100644 index 000000000..b31fbe665 --- /dev/null +++ b/social/apps/cherrypy_app/views.py @@ -0,0 +1,14 @@ +from social.actions import do_auth, do_complete, do_disconnect + + +class CherryPyPSAViews(object): + def auth(self, backend): + return do_auth(self.strategy) + + def complete(self, backend, *args, **kwargs): + # TODO: pass login and pass current user + return do_complete(self.strategy, *args, **kwargs) + + def disconnect(self, backend, association_id=None): + # TODO: pass current user + return do_disconnect(self.strategy, association_id) diff --git a/social/strategies/cherrypy_strategy.py b/social/strategies/cherrypy_strategy.py new file mode 100644 index 000000000..4869f3272 --- /dev/null +++ b/social/strategies/cherrypy_strategy.py @@ -0,0 +1,68 @@ +import six +import cherrypy + +from social.strategies.base import BaseStrategy, BaseTemplateStrategy + + +class CherryPyJinja2TemplateStrategy(BaseTemplateStrategy): + def __init__(self, strategy): + self.strategy = strategy + self.env = self.strategy.get_setting('jinja2env') + + def render_template(self, tpl, context): + return self.env.get_template(tpl).render(context) + + def render_string(self, html, context): + return self.env.from_string(html).render(context) + + +class CherryPyStratety(BaseStrategy): + def __init__(self, *args, **kwargs): + kwargs.setdefault('tpl', CherryPyJinja2TemplateStrategy) + return super(CherryPyStratety, self).__init__(*args, **kwargs) + + def get_setting(self, name): + return cherrypy.request.app.config[name] + + def request_data(self, merge=True): + if merge: + data = cherrypy.request.params + elif cherrypy.request.method == 'POST': + data = cherrypy.body.params + else: + data = cherrypy.request.params + return data + + def request_host(self): + return cherrypy.request.base + + def redirect(self, url): + return cherrypy.HTTPRedirect(url) + + def html(self, content): + return content + + def authenticate(self, *args, **kwargs): + kwargs['strategy'] = self + kwargs['storage'] = self.storage + kwargs['backend'] = self.backend + return self.backend.authenticate(*args, **kwargs) + + def session_get(self, name, default=None): + return cherrypy.session.get(name, default) + + def session_set(self, name, value): + cherrypy.session[name] = value + + def session_pop(self, name): + cherrypy.session.pop(name, None) + + def session_setdefault(self, name, value): + return cherrypy.session.setdefault(name, value) + + def build_absolute_uri(self, path=None): + return cherrypy.url(path or '') + + def is_response(self, value): + return isinstance(value, six.string_types) or \ + isinstance(value, cherrypy.CherryPyException) From 285930eaf842fca2c0b08b5b2be0fc53b16c5c72 Mon Sep 17 00:00:00 2001 From: Branden Rolston Date: Tue, 5 Nov 2013 14:31:37 -0800 Subject: [PATCH 002/890] Use mock. --- social/tests/requirements.txt | 1 + social/tests/test_utils.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index 8531a50d2..c0fcdcc6a 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -1,5 +1,6 @@ httpretty==0.6.5 coverage>=3.6 +mock==1.0.1 nose>=1.2.1 requests>=1.1.0 sure>=1.2.0 diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index 373b668c7..277a310fb 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -1,6 +1,7 @@ import sys import unittest +import mock from sure import expect from social.utils import sanitize_redirect, user_is_authenticated, \ @@ -122,17 +123,15 @@ def test_absolute_uri(self): class PartialPipelineData(unittest.TestCase): - class MockStrategy(object): - request = None - - def session_get(self, name, default=None): - return object() - - def partial_from_session(self, session): - return object(), object(), [], {} def test_kwargs_included_in_result(self): + strategy = mock.Mock() + strategy.session_get.return_value = object() + partial_from_session = (object(), object(), [], {}) + strategy.partial_from_session.return_value = partial_from_session kwargitem = ('foo', 'bar') - _, _, _, xkwargs = partial_pipeline_data(self.MockStrategy(), None, + + _, _, _, xkwargs = partial_pipeline_data(strategy, None, **dict([kwargitem])) + xkwargs.should.have.key(kwargitem[0]).being.equal(kwargitem[1]) From 228b36bb7067306dd2e263abba2318873b3b87fa Mon Sep 17 00:00:00 2001 From: Branden Rolston Date: Tue, 5 Nov 2013 14:33:38 -0800 Subject: [PATCH 003/890] Update partial from session with newer kwargs. --- social/tests/test_utils.py | 10 ++++++++++ social/utils.py | 7 +++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index 277a310fb..a558a68c9 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -135,3 +135,13 @@ def test_kwargs_included_in_result(self): **dict([kwargitem])) xkwargs.should.have.key(kwargitem[0]).being.equal(kwargitem[1]) + + def test_update_user(self): + strategy = mock.Mock() + strategy.session_get.return_value = object() + partial_from_session = (object(), object(), [], {'user': None}) + strategy.partial_from_session.return_value = partial_from_session + user = object() + + _, _, _, xkwargs = partial_pipeline_data(strategy, user) + xkwargs.should.have.key('user').being.equal(user) diff --git a/social/utils.py b/social/utils.py index 3b1f49c3e..912653d88 100644 --- a/social/utils.py +++ b/social/utils.py @@ -128,11 +128,10 @@ def partial_pipeline_data(strategy, user, *args, **kwargs): partial = strategy.session_get('partial_pipeline', None) if partial: idx, backend, xargs, xkwargs = strategy.partial_from_session(partial) - kwargs = kwargs.copy() - kwargs.setdefault('user', user) + kwargs['user'] = user kwargs.setdefault('request', strategy.request) - kwargs.update(xkwargs) - return idx, backend, xargs, kwargs + xkwargs.update(kwargs) + return idx, backend, xargs, xkwargs def build_absolute_uri(host_url, path=None): From 12bbafb9b3acb5c7f2e9430d655120a8c2999eb2 Mon Sep 17 00:00:00 2001 From: Jesse Pollak Date: Wed, 20 Nov 2013 13:49:36 -0800 Subject: [PATCH 004/890] adds clef as a login provider --- docs/backends/clef.rst | 15 ++++++ examples/django_example/example/settings.py | 1 + .../example/templates/home.html | 1 + .../django_me_example/example/settings.py | 1 + .../example/templates/home.html | 1 + examples/flask_example/settings.py | 1 + examples/flask_example/templates/home.html | 1 + examples/pyramid_example/example/settings.py | 1 + .../pyramid_example/example/templates/home.pt | 1 + examples/tornado_example/settings.py | 1 + examples/tornado_example/templates/home.html | 1 + examples/webpy_example/app.py | 1 + examples/webpy_example/templates/home.html | 1 + social/backends/clef.py | 48 +++++++++++++++++++ social/tests/backends/test_clef.py | 29 +++++++++++ 15 files changed, 104 insertions(+) create mode 100644 docs/backends/clef.rst create mode 100644 social/backends/clef.py create mode 100644 social/tests/backends/test_clef.py diff --git a/docs/backends/clef.rst b/docs/backends/clef.rst new file mode 100644 index 000000000..c9f33a397 --- /dev/null +++ b/docs/backends/clef.rst @@ -0,0 +1,15 @@ +Clef +====== + +Clef works similar to Facebook (OAuth). + +- Register a new application at `Clef Developers`_, set the callback URL to + ``http://example.com/complete/github/`` replacing ``example.com`` with your + domain. + +- Fill ``App Id`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_CLEF_KEY = '' + SOCIAL_AUTH_CLEF_SECRET = '' + +.. _Clef Developers: https://getclef.com/developer \ No newline at end of file diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 345cc5fad..913d5fbf5 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -153,6 +153,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', + 'social.backends.clef.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index c8033059d..42febd6af 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -31,6 +31,7 @@ ThisIsMyJam OAuth1
Stocktwits OAuth2
Tripit OAuth
+Clef OAuth
Twilio
Xing OAuth
Yandex OAuth2
diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 2573fe360..30bd3561a 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -160,6 +160,7 @@ 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', 'social.backends.twilio.TwilioAuth', + 'social.backends.clef.ClefOAuth2', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', 'social.backends.douban.DoubanOAuth2', diff --git a/examples/django_me_example/example/templates/home.html b/examples/django_me_example/example/templates/home.html index 9877a67e7..7c11a3a28 100644 --- a/examples/django_me_example/example/templates/home.html +++ b/examples/django_me_example/example/templates/home.html @@ -31,6 +31,7 @@ ThisIsMyJam OAuth1
Stocktwits OAuth2
Tripit OAuth
+Clef OAuth
Twilio
Xing OAuth
Yandex OAuth2
diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 8fd301f93..fcf530c2b 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -46,6 +46,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', + 'social.backends.tripit.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/examples/flask_example/templates/home.html b/examples/flask_example/templates/home.html index a0d3e90e2..bd91538d9 100644 --- a/examples/flask_example/templates/home.html +++ b/examples/flask_example/templates/home.html @@ -30,6 +30,7 @@ ThisIsMyJam OAuth1
Stocktwits OAuth2
Tripit OAuth
+Clef OAuth2
Twilio
Xing OAuth
Yandex OAuth2
diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index ae3a3626f..f798f5085 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -38,6 +38,7 @@ 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', 'social.backends.twilio.TwilioAuth', + 'social.backends.tripit.ClefOAuth2', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', diff --git a/examples/pyramid_example/example/templates/home.pt b/examples/pyramid_example/example/templates/home.pt index 83050b708..cc6494160 100644 --- a/examples/pyramid_example/example/templates/home.pt +++ b/examples/pyramid_example/example/templates/home.pt @@ -34,6 +34,7 @@ ThisIsMyJam OAuth1
Stocktwits OAuth2
Tripit OAuth
+ Clef OAuth2
Twilio
Xing OAuth
Yandex OAuth2
diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index a688ce55f..2181f1b9d 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -36,6 +36,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', + 'social.backends.tripit.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/examples/tornado_example/templates/home.html b/examples/tornado_example/templates/home.html index b54ec8201..daa33d21f 100644 --- a/examples/tornado_example/templates/home.html +++ b/examples/tornado_example/templates/home.html @@ -30,6 +30,7 @@ ThisIsMyJam OAuth1
Stocktwits OAuth2
Tripit OAuth
+Clef OAuth2
Twilio
Xing OAuth
Yandex OAuth2
diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 5321190b7..d7a1dd339 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -49,6 +49,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', + 'social.backends.tripit.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/examples/webpy_example/templates/home.html b/examples/webpy_example/templates/home.html index 7786e83ab..042da0ff5 100644 --- a/examples/webpy_example/templates/home.html +++ b/examples/webpy_example/templates/home.html @@ -30,6 +30,7 @@ ThisIsMyJamm OAuth1
Stocktwits OAuth2
Tripit OAuth
+Clef OAuth2
Twilio
Xing OAuth
Yandex OAuth2
diff --git a/social/backends/clef.py b/social/backends/clef.py new file mode 100644 index 000000000..e2caa76a8 --- /dev/null +++ b/social/backends/clef.py @@ -0,0 +1,48 @@ +""" +Clef OAuth support. + +This contribution adds support for Clef OAuth service. The settings +SOCIAL_AUTH_CLEF_KEY and SOCIAL_AUTH_CLEF_SECRET must be defined with the values +given by Clef application registration process. +""" + +import json +import urllib2 +from social.backends.oauth import BaseOAuth2 + +class ClefOAuth2(BaseOAuth2): + """Clef OAuth authentication backend""" + name = 'clef' + AUTHORIZATION_URL = 'https://clef.io/iframes/qr' + ACCESS_TOKEN_URL = 'https://clef.io/api/v1/authorize' + INFO_URL = 'https://clef.io/api/v1/info' + ACCESS_TOKEN_METHOD = "POST" + SCOPE_SEPARATOR = ',' + EXTRA_DATA = [ + ('id', 'id') + ] + + def auth_params(self, *args, **kwargs): + params = super(ClefOAuth2, self).auth_params(*args, **kwargs) + params['app_id'] = params.pop('client_id') + params['redirect_url'] = params.pop('redirect_uri') + return params + + + def get_user_details(self, response): + """Return user details from Github account""" + info = response.get('info') + return { + 'username': response.get('clef_id'), + 'email': info.get('email', ''), + 'first_name': info.get('first_name'), + 'last_name': info.get('last_name'), + 'phone_number': info.get('phone_number', '') + } + + def user_data(self, access_token, *args, **kwargs): + url = '%s?access_token=%s' % (self.INFO_URL, access_token) + try: + return json.load(urllib2.urlopen(url)) + except ValueError: + return None diff --git a/social/tests/backends/test_clef.py b/social/tests/backends/test_clef.py new file mode 100644 index 000000000..277fe969e --- /dev/null +++ b/social/tests/backends/test_clef.py @@ -0,0 +1,29 @@ +import json + +from httpretty import HTTPretty + +from social.exceptions import AuthFailed + +from social.tests.backends.oauth import OAuth2Test + +class ClefOAuth2Test(OAuth2Test): + backend_path = 'social.backends.clef.ClefOAuth2' + user_data_url = 'https://clef.io/api/v1/info' + expected_username = '123456789' + access_token_body = json.dumps({ + 'access_token': 'foobar' + }) + user_data_body = json.dumps({ + 'info': { + 'first_name': 'Test', + 'last_name': 'User', + 'email': 'test@example.com' + }, + 'clef_id': '123456789' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From cc6970bc1be57fe6b68c57a62bddb59a8a416b52 Mon Sep 17 00:00:00 2001 From: Norton Wang Date: Sat, 23 Nov 2013 12:39:40 -0500 Subject: [PATCH 005/890] Add more examples to django_example, alphabetize, fix some grammar --- examples/django_example/example/settings.py | 81 +++++++----- .../example/templates/home.html | 117 ++++++++++-------- social/backends/box.py | 2 +- social/backends/mailru.py | 2 +- 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 345cc5fad..34a4c9979 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -119,57 +119,70 @@ ) AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.google.GooglePlusAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', + 'social.backends.amazon.AmazonOAuth2', 'social.backends.angel.AngelOAuth2', + 'social.backends.aol.AOLOpenId', + 'social.backends.appsfuel.AppsfuelOAuth2', 'social.backends.behance.BehanceOAuth2', + 'social.backends.belgiumeid.BelgiumEIDOpenId', 'social.backends.bitbucket.BitbucketOAuth', 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.linkedin.LinkedinOAuth2', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', + 'social.backends.douban.DoubanOAuth2', 'social.backends.dropbox.DropboxOAuth', 'social.backends.evernote.EvernoteSandboxOAuth', + 'social.backends.facebook.FacebookAppOAuth2', + 'social.backends.facebook.FacebookOAuth2', + 'social.backends.fedora.FedoraOpenId', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', + 'social.backends.foursquare.FoursquareOAuth2', + 'social.backends.github.GithubOAuth2', + 'social.backends.google.GoogleOAuth', + 'social.backends.google.GoogleOAuth2', + 'social.backends.google.GoogleOpenId', + 'social.backends.google.GooglePlusAuth', + 'social.backends.instagram.InstagramOAuth2', + 'social.backends.jawbone.JawboneOAuth2', + 'social.backends.linkedin.LinkedinOAuth', + 'social.backends.linkedin.LinkedinOAuth2', + 'social.backends.live.LiveOAuth2', 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.douban.DoubanOAuth2', + 'social.backends.mailru.MailruOAuth2', + 'social.backends.mendeley.MendeleyOAuth', 'social.backends.mixcloud.MixcloudOAuth2', + 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', + 'social.backends.open_id.OpenIdAuth', + 'social.backends.orkut.OrkutOAuth', + 'social.backends.persona.PersonaAuth', + 'social.backends.podio.PodioOAuth2', 'social.backends.rdio.RdioOAuth1', 'social.backends.rdio.RdioOAuth2', - 'social.backends.yammer.YammerOAuth2', - 'social.backends.stackoverflow.StackoverflowOAuth2', 'social.backends.readability.ReadabilityOAuth', - 'social.backends.skyrock.SkyrockOAuth', - 'social.backends.tumblr.TumblrOAuth', 'social.backends.reddit.RedditOAuth2', + 'social.backends.runkeeper.RunKeeperOAuth2', + 'social.backends.skyrock.SkyrockOAuth', + 'social.backends.soundcloud.SoundcloudOAuth2', + 'social.backends.stackoverflow.StackoverflowOAuth2', 'social.backends.steam.SteamOpenId', - 'social.backends.podio.PodioOAuth2', - 'social.backends.amazon.AmazonOAuth2', - 'social.backends.runkeeper.RunKeeperOAuth2', + 'social.backends.stocktwits.StocktwitsOAuth2', + 'social.backends.stripe.StripeOAuth2', + 'social.backends.suse.OpenSUSEOpenId', + 'social.backends.thisismyjam.ThisIsMyJamOAuth1', + 'social.backends.trello.TrelloOAuth', + 'social.backends.tripit.TripItOAuth', + 'social.backends.tumblr.TumblrOAuth', + 'social.backends.twilio.TwilioAuth', + 'social.backends.twitter.TwitterOAuth', + 'social.backends.vk.VKOAuth2', + 'social.backends.weibo.WeiboOAuth2', + 'social.backends.xing.XingOAuth', + 'social.backends.yahoo.YahooOAuth', + 'social.backends.yahoo.YahooOpenId', + 'social.backends.yammer.YammerOAuth2', + 'social.backends.yandex.YandexOAuth2', + 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index c8033059d..52b46ac51 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -2,56 +2,69 @@ {% load url from future %} {% block content %} -Google OAuth2
-Google OAuth
-Google OpenId
-Twitter OAuth
-Yahoo OpenId
-Yahoo OAuth
-Stripe OAuth2
-Facebook OAuth2
-Facebook App
-Angel OAuth2
-Behance OAuth2
-Bitbucket OAuth
-Box.net OAuth2
-LinkedIn OAuth
-Github OAuth2
-Foursquare OAuth2
-Instagram OAuth2
-Live OAuth2
-VK.com OAuth2
-Dailymotion OAuth2
-Disqus OAuth2
-Dropbox OAuth
-Evernote OAuth (sandbox mode)
-Fitbit OAuth
-Flickr OAuth
-Soundcloud OAuth2
-ThisIsMyJam OAuth1
-Stocktwits OAuth2
-Tripit OAuth
-Twilio
-Xing OAuth
-Yandex OAuth2
-Douban OAuth2
-Mixcloud OAuth2
-Rdio OAuth2
-Rdio OAuth1
-Yammer OAuth2
-Stackoverflow OAuth2
-Readability OAuth1
-Skyrock OAuth1
-Tumblr OAuth1
-Reddit OAuth2
-Podio OAuth2
-Amazon OAuth2
-Steam OpenId
-Runkeeper OAuth2
-Email Auth
-Username Auth
+Amazon OAuth2
+Angel OAuth2
+AOL OpenId
+Appsfuel OAuth2
+Behance OAuth2
+BelgiumEID OpenId
+Bitbucket OAuth1
+Box.net OAuth2
+Dailymotion OAuth2
+Disqus OAuth2
+Douban OAuth2
+Dropbox OAuth1
+Evernote OAuth (sandbox mode)
+Facebook OAuth2
+Facebook App
+Fedora OpenId
+Fitbit OAuth1
+Flickr OAuth1
+Foursquare OAuth2
+Github OAuth2
+Google OpenId
+Google OAuth1
+Google OAuth2
+Instagram OAuth2
+Jawbone OAuth2
+LinkedIn OAuth1
+Live OAuth2
+Mail.ru OAuth2
+Mendeley OAuth1
+Mixcloud OAuth2
+Odnoklassniki OAuth2
+OpenSUSE OpenId
+Orkut OAuth
+Podio OAuth2
+Rdio OAuth1
+Rdio OAuth2
+Readability OAuth1
+Reddit OAuth2
+Runkeeper OAuth2
+Skyrock OAuth1
+Soundcloud OAuth2
+Stackoverflow OAuth2
+Steam OpenId
+Stocktwits OAuth2
+Stripe OAuth2
+ThisIsMyJam OAuth1
+Trello OAuth1
+Tripit OAuth1
+Tumblr OAuth1
+Twilio
+Twitter OAuth1
+VK.com OAuth2
+Weibo OAuth2
+Xing OAuth1
+Yahoo OpenId
+Yahoo OAuth1
+Yammer OAuth2
+Yandex OAuth2
-
{% csrf_token %} +Email Auth
+Username Auth
+ +{% csrf_token %}
@@ -59,7 +72,7 @@
-
{% csrf_token %} +{% csrf_token %}
@@ -67,13 +80,13 @@
-
{% csrf_token %} +{% csrf_token %} Persona
{% if plus_id %} -
{% csrf_token %} +{% csrf_token %} diff --git a/social/backends/box.py b/social/backends/box.py index dde3b9975..9c01e664a 100644 --- a/social/backends/box.py +++ b/social/backends/box.py @@ -1,7 +1,7 @@ """ Box.net OAuth support. -This contribution adds support for GitHub OAuth service. The settings +This contribution adds support for Box.net OAuth service. The settings SOCIAL_AUTH_BOX_KEY and SOCIAL_AUTH_BOX_SECRET must be defined with the values given by Box.net application registration process. diff --git a/social/backends/mailru.py b/social/backends/mailru.py index 1c00139ec..02c4fe272 100644 --- a/social/backends/mailru.py +++ b/social/backends/mailru.py @@ -1,7 +1,7 @@ """ Mail.ru OAuth2 support -Take a look to http://api.mail.ru/docs/guides/oauth/ +Take a look at http://api.mail.ru/docs/guides/oauth/ You need to register OAuth site here: http://api.mail.ru/sites/my/add From 442ca5ec3ec140268dc34e589a8277d81547478f Mon Sep 17 00:00:00 2001 From: Norton Wang Date: Sat, 23 Nov 2013 15:20:36 -0500 Subject: [PATCH 006/890] add coinbase oauth --- examples/django_example/example/settings.py | 1 + .../example/templates/home.html | 1 + social/apps/django_app/tests.py | 1 + social/backends/coinbase.py | 29 ++++++++++++ social/tests/backends/test_coinbase.py | 47 +++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 social/backends/coinbase.py create mode 100644 social/tests/backends/test_coinbase.py diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 34a4c9979..bdaf1930c 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -127,6 +127,7 @@ 'social.backends.belgiumeid.BelgiumEIDOpenId', 'social.backends.bitbucket.BitbucketOAuth', 'social.backends.box.BoxOAuth2', + 'social.backends.coinbase.CoinbaseOAuth2', 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.douban.DoubanOAuth2', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 52b46ac51..9f2ccc26e 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -10,6 +10,7 @@ BelgiumEID OpenId
Bitbucket OAuth1
Box.net OAuth2
+Coinbase OAuth2
Dailymotion OAuth2
Disqus OAuth2
Douban OAuth2
diff --git a/social/apps/django_app/tests.py b/social/apps/django_app/tests.py index 55cfdbe7c..022d7fc6e 100644 --- a/social/apps/django_app/tests.py +++ b/social/apps/django_app/tests.py @@ -11,6 +11,7 @@ from social.tests.backends.test_bitbucket import * from social.tests.backends.test_box import * from social.tests.backends.test_broken import * +from social.tests.backends.test_coinbase import * from social.tests.backends.test_dailymotion import * from social.tests.backends.test_disqus import * from social.tests.backends.test_dropbox import * diff --git a/social/backends/coinbase.py b/social/backends/coinbase.py new file mode 100644 index 000000000..813fa39f3 --- /dev/null +++ b/social/backends/coinbase.py @@ -0,0 +1,29 @@ +from social.backends.oauth import BaseOAuth2 + + +class CoinbaseOAuth2(BaseOAuth2): + name = 'coinbase' + SCOPE_SEPARATOR = '+' + DEFAULT_SCOPE = ['balance'] + AUTHORIZATION_URL = 'https://coinbase.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://coinbase.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + + def get_user_details(self, response): + """Return user details from Coinbase account""" + user_data = response['users'][0]['user'] + name = user_data['name'] + name_split = name.split() + first_name = name_split[0] + last_name = name_split[1] + email = user_data.get('email', '') + return {'username': name, + 'first_name': first_name, + 'last_name': last_name, + 'email': email} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://coinbase.com/api/v1/users', + params={'access_token': access_token}) diff --git a/social/tests/backends/test_coinbase.py b/social/tests/backends/test_coinbase.py new file mode 100644 index 000000000..edc696ac1 --- /dev/null +++ b/social/tests/backends/test_coinbase.py @@ -0,0 +1,47 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class CoinbaseOAuth2Test(OAuth2Test): + backend_path = 'social.backends.coinbase.CoinbaseOAuth2' + user_data_url = 'https://coinbase.com/api/v1/users' + expected_username = 'SatoshiNakamoto' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'users': [ + { + 'user': { + 'id': "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + 'name': "Satoshi Nakamoto", + 'email': "satoshi@nakamoto.com", + 'pin': None, + 'time_zone': "Eastern Time (US & Canada)", + 'native_currency': "USD", + 'buy_level': 2, + 'sell_level': 2, + 'balance': { + 'amount': "1000000", + 'currency': "BTC" + }, + 'buy_limit': { + 'amount': "50.00000000", + 'currency': "BTC" + }, + 'sell_limit': { + 'amount': "50.00000000", + 'currency': "BTC" + } + } + } + ] + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 0c2cb0ce71286a925dd15d76a09896a6d9922750 Mon Sep 17 00:00:00 2001 From: Norton Wang Date: Sat, 23 Nov 2013 15:41:37 -0500 Subject: [PATCH 007/890] add coinbase docs, add runkeeper docs to index --- docs/backends/coinbase.rst | 21 +++++++++++++++++++++ docs/backends/index.rst | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 docs/backends/coinbase.rst diff --git a/docs/backends/coinbase.rst b/docs/backends/coinbase.rst new file mode 100644 index 000000000..53404424d --- /dev/null +++ b/docs/backends/coinbase.rst @@ -0,0 +1,21 @@ +Coinbase +======== + +Coinbase uses OAuth2. + +- Register an application at Coinbase_ + +- Fill in the **Client Id** and **Client Secret** values in your settings:: + + SOCIAL_AUTH_COINBASE_KEY = '' + SOCIAL_AUTH_COINBASE_SECRET = '' + +- Set the ``redirect_url`` on coinbase. Make sure to include the trailing slash, eg. ``http://hostname/complete/coinbase/`` + +- Specify scopes with:: + + SOCIAL_AUTH_COINBASE_SCOPE = [] + +By default the scope is set to ``balance``. + +.. Coinbase: https://coinbase.com/oauth/applications diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 8b6e7a75e..143c33d88 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -52,6 +52,7 @@ Social backends behance bitbucket box + coinbase disqus douban dropbox @@ -70,6 +71,7 @@ Social backends rdio readability reddit + runkeeper shopify skyrock soundcloud From 325947837ac8daa28bc027c105cf9bc4a67fca68 Mon Sep 17 00:00:00 2001 From: Norton Wang Date: Tue, 26 Nov 2013 20:43:19 -0500 Subject: [PATCH 008/890] fix uid in coinbase oauth --- social/backends/coinbase.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/backends/coinbase.py b/social/backends/coinbase.py index 813fa39f3..30f5feeb9 100644 --- a/social/backends/coinbase.py +++ b/social/backends/coinbase.py @@ -10,6 +10,9 @@ class CoinbaseOAuth2(BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False + def get_user_id(self, details, response): + return response['users'][0]['user']['id'] + def get_user_details(self, response): """Return user details from Coinbase account""" user_data = response['users'][0]['user'] From 74141c5084ab897b5ebe0bc1557eb7a3ca2cd952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 12:47:26 -0200 Subject: [PATCH 009/890] AOL docs --- docs/backends/aol.rst | 11 +++++++++++ docs/backends/index.rst | 1 + 2 files changed, 12 insertions(+) create mode 100644 docs/backends/aol.rst diff --git a/docs/backends/aol.rst b/docs/backends/aol.rst new file mode 100644 index 000000000..86893c54e --- /dev/null +++ b/docs/backends/aol.rst @@ -0,0 +1,11 @@ +AOL +=== + +AOL OpenId doesn't require major settings beside being defined on +``AUTHENTICATION_BACKENDS```:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.aol.AOLOpenId', + ... + ) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 143c33d88..0896b9959 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -48,6 +48,7 @@ Social backends amazon angel + aol appsfuel behance bitbucket From 51c9997d0034089c48bd1ad97b43632fed5ed416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 12:52:21 -0200 Subject: [PATCH 010/890] BelgiumEID docs --- docs/backends/belgium_eid.rst | 11 +++++++++++ docs/backends/index.rst | 1 + 2 files changed, 12 insertions(+) create mode 100644 docs/backends/belgium_eid.rst diff --git a/docs/backends/belgium_eid.rst b/docs/backends/belgium_eid.rst new file mode 100644 index 000000000..a62aca5bb --- /dev/null +++ b/docs/backends/belgium_eid.rst @@ -0,0 +1,11 @@ +Belgium EID +=========== + +Belgium EID OpenId doesn't require major settings beside being defined on +``AUTHENTICATION_BACKENDS```:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.belgiumeid.BelgiumEIDOpenId', + ... + ) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 0896b9959..9470c4818 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -52,6 +52,7 @@ Social backends appsfuel behance bitbucket + belgium_eid box coinbase disqus From 8736be08a8f865c85b8b8e6c80425b0d1eff7594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 12:53:54 -0200 Subject: [PATCH 011/890] Fix backends order --- docs/backends/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 9470c4818..1eb1ec3d6 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -51,8 +51,8 @@ Social backends aol appsfuel behance - bitbucket belgium_eid + bitbucket box coinbase disqus From 0a56c58b0bf476d6bc0a8f5a7bc92238e451e528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:04:38 -0200 Subject: [PATCH 012/890] File format fix to coinbase docs --- docs/backends/coinbase.rst | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/backends/coinbase.rst b/docs/backends/coinbase.rst index 53404424d..b90cdf336 100644 --- a/docs/backends/coinbase.rst +++ b/docs/backends/coinbase.rst @@ -1,21 +1,22 @@ -Coinbase -======== - -Coinbase uses OAuth2. - -- Register an application at Coinbase_ - -- Fill in the **Client Id** and **Client Secret** values in your settings:: - - SOCIAL_AUTH_COINBASE_KEY = '' +Coinbase +======== + +Coinbase uses OAuth2. + +- Register an application at Coinbase_ + +- Fill in the **Client Id** and **Client Secret** values in your settings:: + + SOCIAL_AUTH_COINBASE_KEY = '' SOCIAL_AUTH_COINBASE_SECRET = '' -- Set the ``redirect_url`` on coinbase. Make sure to include the trailing slash, eg. ``http://hostname/complete/coinbase/`` - +- Set the ``redirect_url`` on coinbase. Make sure to include the trailing + slash, eg. ``http://hostname/complete/coinbase/`` + - Specify scopes with:: - SOCIAL_AUTH_COINBASE_SCOPE = [] + SOCIAL_AUTH_COINBASE_SCOPE = [...] -By default the scope is set to ``balance``. - -.. Coinbase: https://coinbase.com/oauth/applications + By default the scope is set to ``balance``. + +.. _Coinbase: https://coinbase.com/oauth/applications From 53d4a87299451703e83f6be4e85c020f6a7acbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:04:48 -0200 Subject: [PATCH 013/890] Dailymotion docs --- docs/backends/dailymotion.rst | 23 +++++++++++++++++++++++ docs/backends/index.rst | 1 + 2 files changed, 24 insertions(+) create mode 100644 docs/backends/dailymotion.rst diff --git a/docs/backends/dailymotion.rst b/docs/backends/dailymotion.rst new file mode 100644 index 000000000..c8198c456 --- /dev/null +++ b/docs/backends/dailymotion.rst @@ -0,0 +1,23 @@ +DailyMotion +=========== + +DailyMotion uses OAuth2. In order to enable the backend follow: + +- Register an application at `DailyMotion Developer Portal`_ + +- Fill in the **Client Id** and **Client Secret** values in your settings:: + + SOCIAL_AUTH_DAILYMOTION_KEY = '' + SOCIAL_AUTH_DAILYMOTION_SECRET = '' + +- Set the ``Callback URL`` to ``http:///complete/dailymotion/`` + +- Specify scopes with:: + + SOCIAL_AUTH_DAILYMOTION_SCOPE = [...] + + Available scopes are listed in the `Requesting Extended Permissions`_ + section. + +.. _DailyMotion Developer Portal: http://www.dailymotion.com/profile/developer/new +.. _Requesting Extended Permissions: http://www.dailymotion.com/doc/api/authentication.html#requesting-extended-permissions diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 1eb1ec3d6..6dd89c8d7 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -55,6 +55,7 @@ Social backends bitbucket box coinbase + dailymotion disqus douban dropbox From 59472e7cd11b3cb798d9a03a87c8977787d20a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:06:55 -0200 Subject: [PATCH 014/890] Fix douban oauth1 title --- docs/backends/douban.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backends/douban.rst b/docs/backends/douban.rst index 7d2d9f3d6..d75d9a2fc 100644 --- a/docs/backends/douban.rst +++ b/docs/backends/douban.rst @@ -3,8 +3,8 @@ Douban Douban supports OAuth 1 and 2. -Douban OAuth 1 --------------- +Douban OAuth1 +------------- Douban OAuth 1 works similar to Twitter OAuth. From 7974df9780e86e7cf4b2ba8bf8efc1a5b2fee8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:11:39 -0200 Subject: [PATCH 015/890] Fedora openid docs --- docs/backends/fedora.rst | 11 +++++++++++ docs/backends/index.rst | 1 + 2 files changed, 12 insertions(+) create mode 100644 docs/backends/fedora.rst diff --git a/docs/backends/fedora.rst b/docs/backends/fedora.rst new file mode 100644 index 000000000..eb6486111 --- /dev/null +++ b/docs/backends/fedora.rst @@ -0,0 +1,11 @@ +Fedora +====== + +Fedora OpenId doesn't require major settings beside being defined on +``AUTHENTICATION_BACKENDS```:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.fedora.FedoraOpenId', + ... + ) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 6dd89c8d7..519669012 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -61,6 +61,7 @@ Social backends dropbox evernote facebook + fedora flickr github google From 73521802558e79881d1b5eb6adf7eb48a338f98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:20:44 -0200 Subject: [PATCH 016/890] Fitbit docs --- docs/backends/fitbit.rst | 15 +++++++++++++++ docs/backends/index.rst | 1 + 2 files changed, 16 insertions(+) create mode 100644 docs/backends/fitbit.rst diff --git a/docs/backends/fitbit.rst b/docs/backends/fitbit.rst new file mode 100644 index 000000000..4218f6af3 --- /dev/null +++ b/docs/backends/fitbit.rst @@ -0,0 +1,15 @@ +Fitbit +====== + +Fitbit offers OAuth1 as their auth mechanism. In order to enable it, follow: + +- Register a new application at `Fitbit dev portal`_, be sure to select + ``Browser`` as the application type. Set the ``Callback URL`` to + ``http:////complete/fitbit/``. + +- Fill **Consumer Key** and **Consumer Secret** values:: + + SOCIAL_AUTH_FITBIT_KEY = '' + SOCIAL_AUTH_FITBIT_SECRET = '' + +.. _Fitbit dev portal: https://dev.fitbit.com/apps/new diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 519669012..5d5f1935c 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -62,6 +62,7 @@ Social backends evernote facebook fedora + fitbit flickr github google From c87418b5aa5b89db01c4cd7d0606b1353c24d318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:25:25 -0200 Subject: [PATCH 017/890] Foursquare backend docs --- docs/backends/foursquare.rst | 14 ++++++++++++++ docs/backends/index.rst | 1 + 2 files changed, 15 insertions(+) create mode 100644 docs/backends/foursquare.rst diff --git a/docs/backends/foursquare.rst b/docs/backends/foursquare.rst new file mode 100644 index 000000000..9dacca2f4 --- /dev/null +++ b/docs/backends/foursquare.rst @@ -0,0 +1,14 @@ +Foursquare +========== + +Foursquare uses OAuth2. In order to enable the backend follow: + +- Register an application at `Foursquare Developers Portal`_, + set the ``Redirect URI`` to ``http:///complete/foursquare/`` + +- Fill in the **Client Id** and **Client Secret** values in your settings:: + + SOCIAL_AUTH_FOURSQUARE_KEY = '' + SOCIAL_AUTH_FOURSQUARE_SECRET = '' + +.. _Foursquare Developers Portal: https://foursquare.com/developers/register diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 5d5f1935c..de5b4211a 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -64,6 +64,7 @@ Social backends fedora fitbit flickr + foursquare github google instagram From a83d9037cde4f555b541c63bab555365f5050e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:43:31 -0200 Subject: [PATCH 018/890] Jawbone docs --- docs/backends/index.rst | 1 + docs/backends/jawbone.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/backends/jawbone.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index de5b4211a..f3d34a1ac 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -68,6 +68,7 @@ Social backends github google instagram + jawbone linkedin live mailru diff --git a/docs/backends/jawbone.rst b/docs/backends/jawbone.rst new file mode 100644 index 000000000..64d9ec206 --- /dev/null +++ b/docs/backends/jawbone.rst @@ -0,0 +1,22 @@ +Jawbone +======= + +Jawbone uses OAuth2. In order to enable the backend follow: + +- Register an application at `Jawbone Developer Portal`_, set the ``OAuth + redirect URIs`` to ``http:///complete/jawbone/`` + +- Fill in the **Client Id** and **Client Secret** values in your settings:: + + SOCIAL_AUTH_JAWBONE_KEY = '' + SOCIAL_AUTH_JAWBONE_SECRET = '' + +- Specify scopes with:: + + SOCIAL_AUTH_JAWBONE_SCOPE = [...] + + Available scopes are listed in the `Jawbone Authentication Reference`_, + "socpes" section. + +.. _Jawbone Developer Portal: https://jawbone.com/up/developer/account/ +.. _Jawbone Authentication Reference: https://jawbone.com/up/developer/authentication From f9768d25f97be17c73015e9d438b11ce987b038c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:48:05 -0200 Subject: [PATCH 019/890] LiveJournal docs --- docs/backends/index.rst | 1 + docs/backends/livejournal.rst | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docs/backends/livejournal.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index f3d34a1ac..4ed16c045 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -71,6 +71,7 @@ Social backends jawbone linkedin live + livejournal mailru mixcloud odnoklassnikiru diff --git a/docs/backends/livejournal.rst b/docs/backends/livejournal.rst new file mode 100644 index 000000000..683ef639a --- /dev/null +++ b/docs/backends/livejournal.rst @@ -0,0 +1,16 @@ +LiveJournal +=========== + +LiveJournal provides OpenId, it doesn't require any major settings in order to +work, beside being defined on ``AUTHENTICATION_BACKENDS```:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.aol.AOLOpenId', + ... + ) + +LiveJournal OpenId is provided by URLs in the form ``http://.livejournal.com``, +this application retrieves the ``username`` from the data in the current +request by checking a parameter named ``openid_lj_user`` which can be sent by +``POST`` or ``GET``. From 3fc091d4e67a6ce182f037b738308bbbe7261a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 13:49:12 -0200 Subject: [PATCH 020/890] Fix backends index order --- docs/backends/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 4ed16c045..fc22b3092 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -70,8 +70,8 @@ Social backends instagram jawbone linkedin - live livejournal + live mailru mixcloud odnoklassnikiru From 2f15d3dd2c079a3ced5f1d03f1ea3f3d8fbac938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 14:18:05 -0200 Subject: [PATCH 021/890] Mendeley docs --- docs/backends/index.rst | 1 + docs/backends/mendeley.rst | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/backends/mendeley.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index fc22b3092..7380df4a1 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -73,6 +73,7 @@ Social backends livejournal live mailru + mendeley mixcloud odnoklassnikiru persona diff --git a/docs/backends/mendeley.rst b/docs/backends/mendeley.rst new file mode 100644 index 000000000..eebcb0c1b --- /dev/null +++ b/docs/backends/mendeley.rst @@ -0,0 +1,13 @@ +Mendeley +======== + +Mendeley works with OAuth1, in order to enable the backend follow: + +- Register a new application at `Mendeley Application Registration`_ + +- Fill **Consumer Key** and **Consumer Secret** values:: + + SOCIAL_AUTH_MENDELEY_KEY = '' + SOCIAL_AUTH_MENDELEY_SECRET = '' + +.. _Mendeley Application Registration: http://dev.mendeley.com/applications/register/ From dbf02edd30100154edb6f13243d57d10e06d84c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 14:51:08 -0200 Subject: [PATCH 022/890] Podio docs --- docs/backends/index.rst | 1 + docs/backends/podio.rst | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/backends/podio.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 7380df4a1..0d6c6f724 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -77,6 +77,7 @@ Social backends mixcloud odnoklassnikiru persona + podio rdio readability reddit diff --git a/docs/backends/podio.rst b/docs/backends/podio.rst new file mode 100644 index 000000000..3f92e5e79 --- /dev/null +++ b/docs/backends/podio.rst @@ -0,0 +1,13 @@ +Podio +===== + +Podio offers OAuth2 as their auth mechanism. In order to enable it, follow: + +- Register a new application at `Podio API Keys`_ + +- Fill **Client Id** and **Client Secret** values:: + + SOCIAL_AUTH_PODIO_KEY = '' + SOCIAL_AUTH_PODIO_SECRET = '' + +.. _Podio API Keys: https://developers.podio.com/api-key From fdf2d0c24806d97ecdb6cc302c07c52667ecb2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 15:07:03 -0200 Subject: [PATCH 023/890] Trello docs --- docs/backends/index.rst | 3 ++- docs/backends/trello.rst | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/backends/trello.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 0d6c6f724..634fefa2f 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -86,11 +86,12 @@ Social backends skyrock soundcloud suse - thisismyjam stackoverflow steam stocktwits stripe + thisismyjam + trello tripit tumblr twilio diff --git a/docs/backends/trello.rst b/docs/backends/trello.rst new file mode 100644 index 000000000..983149b16 --- /dev/null +++ b/docs/backends/trello.rst @@ -0,0 +1,16 @@ +Trello +====== + +Trello provides OAuth1 support for their authentication process. + +In order to enable it, follow: + +- Generate an Application Key pair at `Trello Developers API Keys`_ + +- Fill **Consumer Key** and **Consumer Secret** settings:: + + SOCIAL_AUTH_TRELLO_KEY = '...' + SOCIAL_AUTH_TRELLO_SECRET = '...' + + +.. _Trello Developers API Keys: https://trello.com/1/appKey/generate From f59bcd4fec426e5f4fdcf3472728b2365bb14c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 15:15:15 -0200 Subject: [PATCH 024/890] Xing docs --- docs/backends/index.rst | 1 + docs/backends/xing.rst | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/backends/xing.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 634fefa2f..8c803cb33 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -98,4 +98,5 @@ Social backends twitter vk weibo + xing yahoo diff --git a/docs/backends/xing.rst b/docs/backends/xing.rst new file mode 100644 index 000000000..3454b116b --- /dev/null +++ b/docs/backends/xing.rst @@ -0,0 +1,14 @@ +XING +==== + +XING uses OAuth1 for their auth mechanism, in order to enable the backend +follow: + +- Register a new application at `XING Apps Dashboard`_, + +- Fill **Consumer Key** and **Consumer Secret** values:: + + SOCIAL_AUTH_XING_KEY = '' + SOCIAL_AUTH_XING_SECRET = '' + +.. _XING Apps Dashboard: https://dev.xing.com/applications From 421230479750c1aa26baa9675a9580289dbc70d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 15:18:08 -0200 Subject: [PATCH 025/890] Improves to Yahoo docs --- docs/backends/yahoo.rst | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/backends/yahoo.rst b/docs/backends/yahoo.rst index 32e3a41ec..b0919a484 100644 --- a/docs/backends/yahoo.rst +++ b/docs/backends/yahoo.rst @@ -1,5 +1,24 @@ -Yahoo OAuth -=========== +Yahoo +===== + +Yahoo supports OpenId and OAuth1 for their auth flow. + + +Yahoo OpenId +------------ + +OpenId doesn't require any particular configuration beside enabling the backend +in the ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.yahoo.YahooOpenId', + ... + ) + + +Yahoo OAuth1 +------------ OAuth 1.0 workflow, useful if you are planning to use Yahoo's API. From deabee41562b7a2c279b4c6a7d617ae3727458f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 15:23:32 -0200 Subject: [PATCH 026/890] Yammer docs --- docs/backends/index.rst | 1 + docs/backends/yammer.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 docs/backends/yammer.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 8c803cb33..787a7da3a 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -100,3 +100,4 @@ Social backends weibo xing yahoo + yammer diff --git a/docs/backends/yammer.rst b/docs/backends/yammer.rst new file mode 100644 index 000000000..df6104dbf --- /dev/null +++ b/docs/backends/yammer.rst @@ -0,0 +1,29 @@ +Yammer +====== + +Yammer users OAuth2 for their auth mechanism, this application supports Yammer +OAuth2 in production and staging modes. + +Production Mode +--------------- + +In order to enable the backend, follow: + + +- Register an application at `Client Applications`_ + +- Fill **Client Key** and **Client Secret** settings:: + + SOCIAL_AUTH_YAMMER_KEY = '...' + SOCIAL_AUTH_YAMMER_SECRET = '...' + + +Staging Mode +------------ + +Staging mode is configured the same as ``Production Mode``, but settings are +prefixed with:: + + SOCIAL_AUTH_YAMMER_STAGING_* + +.. _Client Applications: https://www.yammer.com/client_applications From 0434961ffded204380252449f7d3ec03346f08aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 28 Nov 2013 15:25:51 -0200 Subject: [PATCH 027/890] Link to backends docs in the modules instead of repeating the docs. Refs #107 --- social/backends/amazon.py | 4 ++++ social/backends/angel.py | 12 ++---------- social/backends/aol.py | 5 ++--- social/backends/appsfuel.py | 9 ++------- social/backends/base.py | 11 ----------- social/backends/behance.py | 12 ++---------- social/backends/belgiumeid.py | 4 ++++ social/backends/bitbucket.py | 12 ++---------- social/backends/box.py | 13 ++----------- social/backends/coinbase.py | 4 ++++ social/backends/dailymotion.py | 13 ++----------- social/backends/disqus.py | 4 ++++ social/backends/douban.py | 11 ++--------- social/backends/dropbox.py | 10 ++-------- social/backends/email.py | 4 ++++ social/backends/evernote.py | 5 ++--- social/backends/facebook.py | 14 ++------------ social/backends/fedora.py | 5 ++--- social/backends/fitbit.py | 10 ++-------- social/backends/flickr.py | 10 ++-------- social/backends/foursquare.py | 4 ++++ social/backends/github.py | 13 ++----------- social/backends/google.py | 15 ++------------- social/backends/instagram.py | 4 ++++ social/backends/jawbone.py | 4 ++++ social/backends/linkedin.py | 5 ++--- social/backends/live.py | 16 ++-------------- social/backends/livejournal.py | 6 ++---- social/backends/mailru.py | 11 ++--------- social/backends/mendeley.py | 4 ++-- social/backends/mixcloud.py | 3 ++- social/backends/odnoklassniki.py | 18 ++---------------- social/backends/orkut.py | 12 ++---------- social/backends/persona.py | 3 ++- social/backends/podio.py | 6 ++---- social/backends/rdio.py | 4 ++++ social/backends/readability.py | 8 ++------ social/backends/reddit.py | 4 ++-- social/backends/runkeeper.py | 7 ++----- social/backends/shopify.py | 11 ++--------- social/backends/skyrock.py | 11 ++--------- social/backends/soundcloud.py | 14 ++------------ social/backends/stackoverflow.py | 14 ++------------ social/backends/steam.py | 5 ++++- social/backends/stocktwits.py | 4 ++++ social/backends/stripe.py | 9 +++------ social/backends/suse.py | 5 ++--- social/backends/thisismyjam.py | 14 ++------------ social/backends/trello.py | 15 ++------------- social/backends/tripit.py | 10 ++-------- social/backends/tumblr.py | 12 ++---------- social/backends/twilio.py | 3 ++- social/backends/twitter.py | 13 ++----------- social/backends/username.py | 4 ++++ social/backends/vk.py | 6 ++---- social/backends/weibo.py | 14 +++----------- social/backends/xing.py | 5 ++--- social/backends/yahoo.py | 25 ++----------------------- social/backends/yammer.py | 3 ++- 59 files changed, 142 insertions(+), 374 deletions(-) diff --git a/social/backends/amazon.py b/social/backends/amazon.py index c612763b9..fd2d207a6 100644 --- a/social/backends/amazon.py +++ b/social/backends/amazon.py @@ -1,3 +1,7 @@ +""" +Amazon OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/amazon.html +""" from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/angel.py b/social/backends/angel.py index 3ddc5cec6..738bef3a3 100644 --- a/social/backends/angel.py +++ b/social/backends/angel.py @@ -1,14 +1,6 @@ """ -settings.py should include the following: - - ANGEL_CLIENT_ID = '...' - ANGEL_CLIENT_SECRET = '...' - -Optional scope to include 'email' and/or 'messages' separated by space: - - ANGEL_AUTH_EXTRA_ARGUMENTS = {'scope': 'email messages'} - -More information on scope can be found at https://angel.co/api/oauth/faq +Angel OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/angel.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/aol.py b/social/backends/aol.py index a0225f15f..26c901f30 100644 --- a/social/backends/aol.py +++ b/social/backends/aol.py @@ -1,7 +1,6 @@ """ -AOL OpenID support - -No extra configurations are needed to make this work. +AOL OpenId backend, docs at: + http://psa.matiasaguirre.net/docs/backends/aol.html """ from social.backends.open_id import OpenIdAuth diff --git a/social/backends/appsfuel.py b/social/backends/appsfuel.py index 91b82e6db..691e69a2c 100644 --- a/social/backends/appsfuel.py +++ b/social/backends/appsfuel.py @@ -1,11 +1,6 @@ """ -This module is originally written: django-social-auth-appsfuel==1.0.0 -You could refer to https://github.com/AppsFuel/django-social-auth-appsfuel for -issues. - -Needed keys are: - SOCIAL_AUTH_APPSFUEL_KEY = '' - SOCIAL_AUTH_APPSFUEL_SECRET = '' +Appsfueld OAuth2 backend (with sandbox mode support), docs at: + http://psa.matiasaguirre.net/docs/backends/appsfuel.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/base.py b/social/backends/base.py index b2ccc173c..808935ddb 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -1,14 +1,3 @@ -""" -Base backends classes. - -This module defines base classes needed to define custom OpenID or OAuth1/2 -auth services from third parties. This customs must subclass an Auth and a -Backend class, check current implementation for examples. - -Also the modules *must* define a BACKENDS dictionary with the backend name -(which is used for URLs matching) and Auth class, otherwise it won't be -enabled. -""" from requests import request from social.utils import module_member, parse_qs diff --git a/social/backends/behance.py b/social/backends/behance.py index 9d0ac4244..8f915820a 100644 --- a/social/backends/behance.py +++ b/social/backends/behance.py @@ -1,14 +1,6 @@ """ -Behance OAuth2 support. - -This contribution adds support for the Behance OAuth service. The settings -BEHANCE_CLIENT_ID and BEHANCE_CLIENT_SECRET must be defined with the values -given by Behance application registration process. - -Extended permissions are supported by defining BEHANCE_EXTENDED_PERMISSIONS -setting, it must be a list of values to request. - -By default username and access_token are stored in extra_data field. +Behance OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/behance.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/belgiumeid.py b/social/backends/belgiumeid.py index f7b9f130a..1ff44270d 100644 --- a/social/backends/belgiumeid.py +++ b/social/backends/belgiumeid.py @@ -1,3 +1,7 @@ +""" +Belgium EID OpenId backend, docs at: + http://psa.matiasaguirre.net/docs/backends/belgium_eid.html +""" from social.backends.open_id import OpenIdAuth diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index e43ec1f65..db14f84c8 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -1,14 +1,6 @@ """ -Bitbucket OAuth support. - -This adds support for Bitbucket OAuth service. An application must -be registered first on Bitbucket and the settings BITBUCKET_CONSUMER_KEY -and BITBUCKET_CONSUMER_SECRET must be defined with the corresponding -values. - -By default username, email, token expiration time, first name and last name are -stored in extra_data field, check OAuthBackend class for details on how to -extend it. +Bitbucket OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/bitbucket.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/box.py b/social/backends/box.py index 9c01e664a..5b82f5d91 100644 --- a/social/backends/box.py +++ b/social/backends/box.py @@ -1,15 +1,6 @@ """ -Box.net OAuth support. - -This contribution adds support for Box.net OAuth service. The settings -SOCIAL_AUTH_BOX_KEY and SOCIAL_AUTH_BOX_SECRET must be defined with the values -given by Box.net application registration process. - -Extended permissions are supported by defining BOX_EXTENDED_PERMISSIONS -setting, it must be a list of values to request. - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +Box.net OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/box.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/coinbase.py b/social/backends/coinbase.py index 30f5feeb9..22546c396 100644 --- a/social/backends/coinbase.py +++ b/social/backends/coinbase.py @@ -1,3 +1,7 @@ +""" +Coinbase OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/coinbase.html +""" from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/dailymotion.py b/social/backends/dailymotion.py index f8dd40bc2..bef394dc1 100644 --- a/social/backends/dailymotion.py +++ b/social/backends/dailymotion.py @@ -1,15 +1,6 @@ """ -Dailymotion OAuth2 support. - -This adds support for Dailymotion OAuth service. An application must -be registered first on dailymotion and the settings DAILYMOTION_CONSUMER_KEY -and DAILYMOTION_CONSUMER_SECRET must be defined with the corresponding -values. - -User screen name is used to generate username. - -By default account id is stored in extra_data field, check OAuthBackend -class for details on how to extend it. +DailyMotion OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/dailymotion.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/disqus.py b/social/backends/disqus.py index a3cc1f9b1..df516b1cc 100644 --- a/social/backends/disqus.py +++ b/social/backends/disqus.py @@ -1,3 +1,7 @@ +""" +Disqus OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/disqus.html +""" from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/douban.py b/social/backends/douban.py index dab350cc6..3f274534a 100644 --- a/social/backends/douban.py +++ b/social/backends/douban.py @@ -1,13 +1,6 @@ """ -Douban OAuth support. - -This adds support for Douban OAuth service. An application must -be registered first on douban.com and the settings DOUBAN_CONSUMER_KEY -and DOUBAN_CONSUMER_SECRET must be defined with they corresponding -values. - -By default account id is stored in extra_data field, check OAuthBackend -class for details on how to extend it. +Douban OAuth1 and OAuth2 backends, docs at: + http://psa.matiasaguirre.net/docs/backends/douban.html """ from social.backends.oauth import BaseOAuth2, BaseOAuth1 diff --git a/social/backends/dropbox.py b/social/backends/dropbox.py index 37fc0aeca..3ac9014d7 100644 --- a/social/backends/dropbox.py +++ b/social/backends/dropbox.py @@ -1,12 +1,6 @@ """ -Dropbox OAuth support. - -This contribution adds support for Dropbox OAuth service. The settings -DROPBOX_APP_ID and DROPBOX_API_SECRET must be defined with the values -given by Dropbox application registration process. - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +Dropbox OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/dropbox.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/email.py b/social/backends/email.py index 5ae19f7a7..4666499cb 100644 --- a/social/backends/email.py +++ b/social/backends/email.py @@ -1,3 +1,7 @@ +""" +Legacy Email backend, docs at: + http://psa.matiasaguirre.net/docs/backends/email.html +""" from social.backends.legacy import LegacyAuth diff --git a/social/backends/evernote.py b/social/backends/evernote.py index 3fec57066..d3eb59898 100644 --- a/social/backends/evernote.py +++ b/social/backends/evernote.py @@ -1,7 +1,6 @@ """ -EverNote OAuth support - -No extra configurations are needed to make this work. +Evernote OAuth1 backend (with sandbox mode support), docs at: + http://psa.matiasaguirre.net/docs/backends/evernote.html """ from requests import HTTPError diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 27c709da4..77e5e7717 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -1,16 +1,6 @@ """ -Facebook OAuth support. - -This contribution adds support for Facebook OAuth service. The settings -SOCIAL_AUTH_FACEBOOK_KEY and SOCIAL_AUTH_FACEBOOK_SECRET must be defined with -the values given by Facebook application registration process. - -Extended permissions are supported by defining -SOCIAL_AUTH_FACEBOOK_EXTENDED_PERMISSIONS setting, it must be a list of values -to request. - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +Facebook OAuth2 and Canvas Application backends, docs at: + http://psa.matiasaguirre.net/docs/backends/facebook.html """ import hmac import time diff --git a/social/backends/fedora.py b/social/backends/fedora.py index 0ca62d60c..38ed9bfbd 100644 --- a/social/backends/fedora.py +++ b/social/backends/fedora.py @@ -1,7 +1,6 @@ """ -Fedora OpenID support - -No extra configurations are needed to make this work. +Fedora OpenId backend, docs at: + http://psa.matiasaguirre.net/docs/backends/fedora.html """ from social.backends.open_id import OpenIdAuth diff --git a/social/backends/fitbit.py b/social/backends/fitbit.py index 8bf640b68..1e808988b 100644 --- a/social/backends/fitbit.py +++ b/social/backends/fitbit.py @@ -1,12 +1,6 @@ """ -Fitbit OAuth support. - -This contribution adds support for Fitbit OAuth service. The settings -FITBIT_CONSUMER_KEY and FITBIT_CONSUMER_SECRET must be defined with the values -given by Fitbit application registration process. - -By default account id, username and token expiration time are stored in -extra_data field, check OAuthBackend class for details on how to extend it. +Fitbit OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/fitbit.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/flickr.py b/social/backends/flickr.py index 5463dee4d..56aa4f6e8 100644 --- a/social/backends/flickr.py +++ b/social/backends/flickr.py @@ -1,12 +1,6 @@ """ -Flickr OAuth support. - -This contribution adds support for Flickr OAuth service. The settings -FLICKR_APP_ID and FLICKR_API_SECRET must be defined with the values -given by Flickr application registration process. - -By default account id, username and token expiration time are stored in -extra_data field, check OAuthBackend class for details on how to extend it. +Flickr OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/flickr.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/foursquare.py b/social/backends/foursquare.py index b3b505f3d..67d4672ce 100644 --- a/social/backends/foursquare.py +++ b/social/backends/foursquare.py @@ -1,3 +1,7 @@ +""" +Foursquare OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/foursquare.html +""" from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/github.py b/social/backends/github.py index 3ef27529d..10feeec0b 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -1,15 +1,6 @@ """ -GitHub OAuth support. - -This contribution adds support for GitHub OAuth service. The settings -GITHUB_APP_ID and GITHUB_API_SECRET must be defined with the values -given by GitHub application registration process. - -Extended permissions are supported by defining GITHUB_EXTENDED_PERMISSIONS -setting, it must be a list of values to request. - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +Github OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/github.html """ from requests import HTTPError diff --git a/social/backends/google.py b/social/backends/google.py index 8467beed2..a551615bd 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -1,17 +1,6 @@ """ -Google OpenID and OAuth support - -OAuth works straightforward using anonymous configurations, username -is generated by requesting email to the not documented, googleapis.com -service. Registered applications can define settings GOOGLE_CONSUMER_KEY -and GOOGLE_CONSUMER_SECRET and they will be used in the auth process. -Setting GOOGLE_OAUTH_SCOPE can be used to access different user -related data, like calendar, contacts, docs, etc. - -OAuth2 works similar to OAuth but application must be defined on Google -APIs console https://code.google.com/apis/console/ Identity option. - -OpenID also works straightforward, it doesn't need further configurations. +Google OpenId, OAuth2, OAuth1, Google+ Sign-in backends, docs at: + http://psa.matiasaguirre.net/docs/backends/google.html """ from requests import HTTPError diff --git a/social/backends/instagram.py b/social/backends/instagram.py index a0a17ef85..a6752ea4a 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -1,3 +1,7 @@ +""" +Instagram OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/instagram.html +""" from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index d92bfe5d0..34243429a 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -1,3 +1,7 @@ +""" +Jawbone OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/jawbone.html +""" from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthCanceled, AuthUnknownError diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index 1f6ef40d1..7ab99381e 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -1,7 +1,6 @@ """ -Linkedin OAuth support - -No extra configurations are needed to make this work. +LinkedIn OAuth1 and OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/linkedin.html """ from social.backends.oauth import BaseOAuth1, BaseOAuth2 diff --git a/social/backends/live.py b/social/backends/live.py index b4604b611..2884438fb 100644 --- a/social/backends/live.py +++ b/social/backends/live.py @@ -1,18 +1,6 @@ """ -MSN Live Connect oAuth 2.0 - -Settings: -LIVE_CLIENT_ID -LIVE_CLIENT_SECRET -LIVE_EXTENDED_PERMISSIONS (defaults are: wl.basic, wl.emails) - -References: -* oAuth http://msdn.microsoft.com/en-us/library/live/hh243649.aspx -* Scopes http://msdn.microsoft.com/en-us/library/live/hh243646.aspx -* REST http://msdn.microsoft.com/en-us/library/live/hh243648.aspx - -Throws: -AuthUnknownError - if user data retrieval fails +Live OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/live.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/livejournal.py b/social/backends/livejournal.py index 4da825317..c2e92d117 100644 --- a/social/backends/livejournal.py +++ b/social/backends/livejournal.py @@ -1,8 +1,6 @@ """ -LiveJournal OpenID support. - -This contribution adds support for LiveJournal OpenID service in the form -username.livejournal.com. Username is retrieved from the identity url. +LiveJournal OpenId backend, docs at: + http://psa.matiasaguirre.net/docs/backends/livejournal.html """ from social.p3 import urlsplit from social.backends.open_id import OpenIdAuth diff --git a/social/backends/mailru.py b/social/backends/mailru.py index 02c4fe272..7d6eaa859 100644 --- a/social/backends/mailru.py +++ b/social/backends/mailru.py @@ -1,13 +1,6 @@ """ -Mail.ru OAuth2 support - -Take a look at http://api.mail.ru/docs/guides/oauth/ - -You need to register OAuth site here: -http://api.mail.ru/sites/my/add - -Then update your settings values using registration information - +Mail.ru OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/mailru.html """ from hashlib import md5 diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index 5bb3c3a52..eb87200eb 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -1,6 +1,6 @@ """ -Mendeley OAuth support -No extra configurations are needed to make this work. +Mendeley OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/mendeley.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/mixcloud.py b/social/backends/mixcloud.py index dfdd565ea..9cf9f84e6 100644 --- a/social/backends/mixcloud.py +++ b/social/backends/mixcloud.py @@ -1,5 +1,6 @@ """ -Mixcloud OAuth2 support +Mixcloud OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/mixcloud.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 46a2ebf62..13e3d0c44 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -1,20 +1,6 @@ """ -Odnoklassniki.ru OAuth2 and IFRAME application support -If you are using OAuth2 authentication, - * Take a look to: - http://dev.odnoklassniki.ru/wiki/display/ok/The+OAuth+2.0+Protocol - * You need to register OAuth application here: - http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=13992188 -elif you're building iframe application, - * Take a look to: - http://dev.odnoklassniki.ru/wiki/display/ok/ - Odnoklassniki.ru+Third+Party+Platform - * You need to register your iframe application here: - http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=5668937 - * You need to sign a public offer and do some bureaucracy if you want to be - listed in application registry -Then setup your application according manual and use information from -registration mail to set settings values. +Odnoklassniki OAuth2 and Iframe Application backends, docs at: + http://psa.matiasaguirre.net/docs/backends/odnoklassnikiru.html """ from hashlib import md5 diff --git a/social/backends/orkut.py b/social/backends/orkut.py index 2571ecb3d..00b4d6001 100644 --- a/social/backends/orkut.py +++ b/social/backends/orkut.py @@ -1,14 +1,6 @@ """ -Orkut OAuth support. - -This contribution adds support for Orkut OAuth service. The scope is -limited to http://orkut.gmodules.com/social/ by default, but can be -extended with ORKUT_SCOPE on project settings. Also name, display -name and emails are the default requested user data, but extra values -can be specified by defining ORKUT_EXTRA_DATA setting. - -OAuth settings ORKUT_CONSUMER_KEY and ORKUT_CONSUMER_SECRET are needed -to enable this service support. +Orkut OAuth backend, docs at: + http://psa.matiasaguirre.net/docs/backends/google.html#orkut """ from social.backends.google import GoogleOAuth diff --git a/social/backends/persona.py b/social/backends/persona.py index eb3c3ea91..0bafa639c 100644 --- a/social/backends/persona.py +++ b/social/backends/persona.py @@ -1,5 +1,6 @@ """ -BrowserID support +Mozilla Persona authentication backend, docs at: + http://psa.matiasaguirre.net/docs/backends/persona.html """ from social.backends.base import BaseAuth from social.exceptions import AuthFailed, AuthMissingParameter diff --git a/social/backends/podio.py b/social/backends/podio.py index 1d3f80467..46af3068c 100644 --- a/social/backends/podio.py +++ b/social/backends/podio.py @@ -1,9 +1,7 @@ """ -Podio OAuth2 support - -https://developers.podio.com/authentication/server_side +Podio OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/podio.html """ - from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/rdio.py b/social/backends/rdio.py index f01a55579..51e475b7a 100644 --- a/social/backends/rdio.py +++ b/social/backends/rdio.py @@ -1,3 +1,7 @@ +""" +Rdio OAuth1 and OAuth2 backends, docs at: + http://psa.matiasaguirre.net/docs/backends/rdio.html +""" from social.backends.oauth import BaseOAuth1, BaseOAuth2, OAuthAuth diff --git a/social/backends/readability.py b/social/backends/readability.py index 96f75668e..72f7f785a 100644 --- a/social/backends/readability.py +++ b/social/backends/readability.py @@ -1,10 +1,6 @@ """ -Readability OAuth support. - -This contribution adds support for Readability OAuth service. The settings -SOCIAL_AUTH_READABILITY_CONSUMER_KEY and -SOCIAL_AUTH_READABILITY_CONSUMER_SECRET must be defined with the values given -by Readability in the Connections page of your account settings. +Readability OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/readability.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/reddit.py b/social/backends/reddit.py index 5709683b8..3024f1134 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -1,6 +1,6 @@ """ -Reddit OAuth2 support as detailed at: - https://github.com/reddit/reddit/wiki/OAuth2 +Reddit OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/reddit.html """ import base64 diff --git a/social/backends/runkeeper.py b/social/backends/runkeeper.py index 043744f66..2ecb0a06a 100644 --- a/social/backends/runkeeper.py +++ b/social/backends/runkeeper.py @@ -1,9 +1,6 @@ """ -RunKeeper OAuth support. - -This contribution adds support for RunKeeper Oauth service. The settings -SOCIAL_AUTH_RUNKEEPER_KEY and SOCIAL_AUTH_RUNKEEPER_SECRET must be defined with -the values given by RunKeeper application registration process. +RunKeeper OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/runkeeper.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 2faa07b51..61212b9b6 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -1,13 +1,6 @@ """ -Shopify OAuth support. - -You must: - -- Register an App in the shopify partner control panel -- Add the API Key and shared secret in your django settings -- Set the Application URL in shopify app settings -- Install the shopify package - +Shopify OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/shopify.html """ import imp import six diff --git a/social/backends/skyrock.py b/social/backends/skyrock.py index 7b85fe08f..3492e3155 100644 --- a/social/backends/skyrock.py +++ b/social/backends/skyrock.py @@ -1,13 +1,6 @@ """ -Skyrock OAuth support. - -This adds support for Skyrock OAuth service. An application must -be registered first on skyrock and the settings SKYROCK_CONSUMER_KEY -and SKYROCK_CONSUMER_SECRET must be defined with they corresponding -values. - -By default account id is stored in extra_data field, check OAuthBackend -class for details on how to extend it. +Skyrock OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/skyrock.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/soundcloud.py b/social/backends/soundcloud.py index aec75e888..2c43234cd 100644 --- a/social/backends/soundcloud.py +++ b/social/backends/soundcloud.py @@ -1,16 +1,6 @@ """ -SoundCloud OAuth2 support. - -This contribution adds support for SoundCloud OAuth2 service. - -The settings SOUNDCLOUD_CLIENT_ID & SOUNDCLOUD_CLIENT_SECRET must be defined -with the values given by SoundCloud application registration process. - -http://developers.soundcloud.com/ -http://developers.soundcloud.com/docs - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +Soundcloud OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/soundcloud.html """ from social.p3 import urlencode from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/stackoverflow.py b/social/backends/stackoverflow.py index 479a690d6..5ddd0d103 100644 --- a/social/backends/stackoverflow.py +++ b/social/backends/stackoverflow.py @@ -1,16 +1,6 @@ """ -Stackoverflow OAuth support. - -This contribution adds support for Stackoverflow OAuth service. The settings -SOCIAL_AUTH_STACKOVERFLOW_KEY, SOCIAL_AUTH_STACKOVERFLOW_SECRET and -SOCIAL_AUTH_STACKOVERFLOW_API_KEY must be defined with the values given by -Stackoverflow application registration process. - -Extended permissions are supported by defining SOCIAL_AUTH_STACKOVERFLOW_SCOPE -setting, it must be a list of values to request. - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +Stackoverflow OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/stackoverflow.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/steam.py b/social/backends/steam.py index cbc94b452..8ae43b9bc 100644 --- a/social/backends/steam.py +++ b/social/backends/steam.py @@ -1,4 +1,7 @@ -"""Steam OpenId support""" +""" +Steam OpenId backend, docs at: + http://psa.matiasaguirre.net/docs/backends/steam.html +""" from social.backends.open_id import OpenIdAuth from social.exceptions import AuthFailed diff --git a/social/backends/stocktwits.py b/social/backends/stocktwits.py index adfe56788..601272f31 100644 --- a/social/backends/stocktwits.py +++ b/social/backends/stocktwits.py @@ -1,3 +1,7 @@ +""" +Stocktwits OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/stocktwits.html +""" from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/stripe.py b/social/backends/stripe.py index c0c3bb1dc..560c1390c 100644 --- a/social/backends/stripe.py +++ b/social/backends/stripe.py @@ -1,9 +1,6 @@ """ -Stripe OAuth2 support. - -This backend adds support for Stripe OAuth2 service. The settings -STRIPE_APP_ID and STRIPE_API_SECRET must be defined with the values -given by Stripe application registration process. +Stripe OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/stripe.html """ from social.backends.oauth import BaseOAuth2 @@ -45,7 +42,7 @@ def auth_complete_params(self, state=None): 'client_id': client_id, 'scope': self.SCOPE_SEPARATOR.join(self.get_scope()), 'code': self.data['code'] - } + } def auth_headers(self): client_id, client_secret = self.get_key_and_secret() diff --git a/social/backends/suse.py b/social/backends/suse.py index 1f77a07b0..0e2d56b58 100644 --- a/social/backends/suse.py +++ b/social/backends/suse.py @@ -1,7 +1,6 @@ """ -OpenSUSE OpenID support - -OpenID also works straightforward, it doesn't need further configurations. +Open Suse OpenId backend, docs at: + http://psa.matiasaguirre.net/docs/backends/suse.html """ from social.backends.open_id import OpenIdAuth diff --git a/social/backends/thisismyjam.py b/social/backends/thisismyjam.py index d51bfc7ce..9ee5094f8 100644 --- a/social/backends/thisismyjam.py +++ b/social/backends/thisismyjam.py @@ -1,16 +1,6 @@ """ -ThisIsMyJam OAuth support. - -This contribution adds support for ThisIsMyJam service. - -The settings SOCIAL_AUTH_THISISMYJAM_KEY & SOCIAL_AUTH_THISISMYJAM_SECRET must -be defined with the values given by SoundCloud application registration -process. - -http://www.thisismyjam.com/developers - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. +ThisIsMyJam OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/thisismyjam.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/trello.py b/social/backends/trello.py index 25e700c7f..cc069acf2 100644 --- a/social/backends/trello.py +++ b/social/backends/trello.py @@ -1,18 +1,7 @@ """ -Trello OAuth support. - -This contribution adds support for Trello OAuth service. The settings -SOCIAL_AUTH_TRELLO_KEY and SOCIAL_AUTH_TRELLO_SECRET must be defined with -the values given by `https://trello.com/1/appKey/generate`. - -Extended permissions are supported by defining TRELLO_EXTENDED_PERMISSIONS -setting, it must be a list of values to request. - -By default account id and token expiration time are stored in extra_data -field, check OAuthBackend class for details on how to extend it. - +Trello OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/trello.html """ - from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/tripit.py b/social/backends/tripit.py index 1dae0db51..566400d00 100644 --- a/social/backends/tripit.py +++ b/social/backends/tripit.py @@ -1,12 +1,6 @@ """ -TripIt OAuth support. - -This adds support for TripIt OAuth service. An application must -be registered first on TripIt and the settings TRIPIT_API_KEY -and TRIPIT_API_SECRET must be defined with the corresponding -values. - -User screen name is used to generate username. +Tripit OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/tripit.html """ from xml.dom import minidom diff --git a/social/backends/tumblr.py b/social/backends/tumblr.py index f292a035b..2e62a37ff 100644 --- a/social/backends/tumblr.py +++ b/social/backends/tumblr.py @@ -1,14 +1,6 @@ """ -Tumblr OAuth 1.0a support. - -Take a look to http://www.tumblr.com/docs/en/api/v2 - -You need to register OAuth site here: http://www.tumblr.com/oauth/apps - -Then update your settings values using registration information - -ref: - https://github.com/gkmngrgn/django-tumblr-auth +Tumblr OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/tumblr.html """ from social.utils import first from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/twilio.py b/social/backends/twilio.py index 9b31fa3ea..96621dd7e 100644 --- a/social/backends/twilio.py +++ b/social/backends/twilio.py @@ -1,5 +1,6 @@ """ -Twilio support +Amazon auth backend, docs at: + http://psa.matiasaguirre.net/docs/backends/twilio.html """ from re import sub diff --git a/social/backends/twitter.py b/social/backends/twitter.py index 942abe9e3..284e8fb5d 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -1,15 +1,6 @@ """ -Twitter OAuth support. - -This adds support for Twitter OAuth service. An application must -be registered first on twitter and the settings TWITTER_CONSUMER_KEY -and TWITTER_CONSUMER_SECRET must be defined with the corresponding -values. - -User screen name is used to generate username. - -By default account id is stored in extra_data field, check OAuthBackend -class for details on how to extend it. +Twitter OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/twitter.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/username.py b/social/backends/username.py index ba093c0f4..d519cfe43 100644 --- a/social/backends/username.py +++ b/social/backends/username.py @@ -1,3 +1,7 @@ +""" +Legacy Username backend, docs at: + http://psa.matiasaguirre.net/docs/backends/username.html +""" from social.backends.legacy import LegacyAuth diff --git a/social/backends/vk.py b/social/backends/vk.py index a8112f49b..2f7bbcd29 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- """ -VK.com (former Vkontakte) OpenAPI and OAuth 2.0 support. - -This backend adds support for VK.com OpenAPI, OAuth2 and OAuth2 for IFrame -applications. +VK.com OpenAPI, OAuth2 and Iframe application OAuth2 backends, docs at: + http://psa.matiasaguirre.net/docs/backends/vk.html """ from time import time from hashlib import md5 diff --git a/social/backends/weibo.py b/social/backends/weibo.py index a2afa5ad8..c9cec8c47 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -1,16 +1,8 @@ #coding:utf8 -#author:hepochen@gmail.com https://github.com/hepochen +# author:hepochen@gmail.com https://github.com/hepochen """ -Weibo OAuth2 support. - -This script adds support for Weibo OAuth service. An application must -be registered first on http://open.weibo.com. - -WEIBO_CLIENT_KEY and WEIBO_CLIENT_SECRET must be defined in the settings.py -correctly. - -By default account id,profile_image_url,gender are stored in extra_data field, -check OAuthBackend class for details on how to extend it. +Weibo OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/weibo.html """ from social.backends.oauth import BaseOAuth2 diff --git a/social/backends/xing.py b/social/backends/xing.py index a556eccab..574690190 100644 --- a/social/backends/xing.py +++ b/social/backends/xing.py @@ -1,7 +1,6 @@ """ -XING OAuth support - -No extra configurations are needed to make this work. +XING OAuth1 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/xing.html """ from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index f2e955d3f..60356b7bf 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -1,27 +1,6 @@ """ -Yahoo OpenID support - - No extra configurations are needed to make this work. - -OAuth 1.0 Yahoo backend - - Options: - YAHOO_CONSUMER_KEY - YAHOO_CONSUMER_SECRET - - References: - * http://developer.yahoo.com/oauth/guide/oauth-auth-flow.html - * http://developer.yahoo.com/social/rest_api_guide/ - * introspective-guid-resource.html - * http://developer.yahoo.com/social/rest_api_guide/ - * extended-profile-resource.html - - Scopes: - To make this extension works correctly you have to have at least - Yahoo Profile scope with Read permission - - Throws: - AuthUnknownError - if user data retrieval fails (guid or profile) +Yahoo OpenId and OAuth1 backends, docs at: + http://psa.matiasaguirre.net/docs/backends/yahoo.html """ from social.backends.open_id import OpenIdAuth from social.backends.oauth import BaseOAuth1 diff --git a/social/backends/yammer.py b/social/backends/yammer.py index 055458f04..adcdde6f2 100644 --- a/social/backends/yammer.py +++ b/social/backends/yammer.py @@ -1,5 +1,6 @@ """ -Yammer OAuth2 support +Yammer OAuth2 production and staging backends, docs at: + http://psa.matiasaguirre.net/docs/backends/yammer.html """ from social.backends.oauth import BaseOAuth2 From 7908c7ba016be99726fa469b6507adbdee887469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 Nov 2013 20:55:28 -0200 Subject: [PATCH 028/890] PEP8 --- examples/django_example/example/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index bdaf1930c..d0087ff80 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -162,7 +162,7 @@ 'social.backends.rdio.RdioOAuth2', 'social.backends.readability.ReadabilityOAuth', 'social.backends.reddit.RedditOAuth2', - 'social.backends.runkeeper.RunKeeperOAuth2', + 'social.backends.runkeeper.RunKeeperOAuth2', 'social.backends.skyrock.SkyrockOAuth', 'social.backends.soundcloud.SoundcloudOAuth2', 'social.backends.stackoverflow.StackoverflowOAuth2', @@ -183,7 +183,6 @@ 'social.backends.yahoo.YahooOpenId', 'social.backends.yammer.YammerOAuth2', 'social.backends.yandex.YandexOAuth2', - 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', From 196e17f2cd65aba028398bc903a305b2841f14ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 30 Nov 2013 16:56:48 -0200 Subject: [PATCH 029/890] Set current strategy on webpy and flask apps --- social/apps/flask_app/__init__.py | 5 +++++ social/apps/webpy_app/__init__.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/social/apps/flask_app/__init__.py b/social/apps/flask_app/__init__.py index e69de29bb..e98cdc1cc 100644 --- a/social/apps/flask_app/__init__.py +++ b/social/apps/flask_app/__init__.py @@ -0,0 +1,5 @@ +from social.strategies.utils import set_current_strategy_getter +from social.apps.flask_app.utils import load_strategy + + +set_current_strategy_getter(load_strategy) diff --git a/social/apps/webpy_app/__init__.py b/social/apps/webpy_app/__init__.py index e69de29bb..e98cdc1cc 100644 --- a/social/apps/webpy_app/__init__.py +++ b/social/apps/webpy_app/__init__.py @@ -0,0 +1,5 @@ +from social.strategies.utils import set_current_strategy_getter +from social.apps.flask_app.utils import load_strategy + + +set_current_strategy_getter(load_strategy) From 2cf8d66a0a5662895c33d520c637601b67eb24df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 30 Nov 2013 17:07:46 -0200 Subject: [PATCH 030/890] Simplify pyramid settings access --- social/apps/pyramid_app/utils.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/social/apps/pyramid_app/utils.py b/social/apps/pyramid_app/utils.py index 972c9a09e..37659b391 100644 --- a/social/apps/pyramid_app/utils.py +++ b/social/apps/pyramid_app/utils.py @@ -1,5 +1,6 @@ from functools import wraps +from pyramid.threadlocal import get_current_registry from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden from social.utils import setting_name, module_member @@ -13,17 +14,16 @@ } -def get_helper(request, name): - return request.registry.settings.get(setting_name(name), - DEFAULTS.get(name, None)) +def get_helper(name): + settings = get_current_registry().settings + return settings.get(setting_name(name), DEFAULTS.get(name, None)) -def load_strategy(request, *args, **kwargs): - backends = get_helper(request, 'AUTHENTICATION_BACKENDS') - strategy = get_helper(request, 'STRATEGY') - storage = get_helper(request, 'STORAGE') - return get_strategy(backends, strategy, storage, request=request, - *args, **kwargs) +def load_strategy(*args, **kwargs): + backends = get_helper('AUTHENTICATION_BACKENDS') + strategy = get_helper('STRATEGY') + storage = get_helper('STORAGE') + return get_strategy(backends, strategy, storage, *args, **kwargs) def strategy(redirect_uri=None): @@ -37,8 +37,10 @@ def wrapper(request, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = request.route_url(uri, backend=backend) - request.strategy = load_strategy(request, backend=backend, - redirect_uri=uri, *args, **kwargs) + request.strategy = load_strategy( + backend=backend, redirect_uri=uri, request=request, + *args, **kwargs + ) return func(request, *args, **kwargs) return wrapper return decorator @@ -59,9 +61,9 @@ def wrapper(request, *args, **kwargs): def backends(request, user): """Load Social Auth current user data to context under the key 'backends'. Will return the output of social.backends.utils.user_backends_data.""" - storage = module_member(get_helper(request, 'STORAGE')) + storage = module_member(get_helper('STORAGE')) return { 'backends': user_backends_data( - user, get_helper(request, 'AUTHENTICATION_BACKENDS'), storage + user, get_helper('AUTHENTICATION_BACKENDS'), storage ) } From 72a5f58d7c7fe18f5ce4c2e02cf8a26146777f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 30 Nov 2013 17:09:41 -0200 Subject: [PATCH 031/890] Set current strategy on pyramid app --- social/apps/pyramid_app/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/social/apps/pyramid_app/__init__.py b/social/apps/pyramid_app/__init__.py index 8b1f15b57..4ea333f87 100644 --- a/social/apps/pyramid_app/__init__.py +++ b/social/apps/pyramid_app/__init__.py @@ -1,6 +1,13 @@ +from social.strategies.utils import set_current_strategy_getter +from social.apps.pyramid_app.utils import load_strategy + + def includeme(config): config.add_route('social.auth', '/login/{backend}') config.add_route('social.complete', '/complete/{backend}') config.add_route('social.disconnect', '/disconnect/{backend}') config.add_route('social.disconnect_association', '/disconnect/{backend}/{association_id}') + + +set_current_strategy_getter(load_strategy) From 07124e73217883ebbae9980b6e749a554e2f0d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 2 Dec 2013 11:40:49 -0200 Subject: [PATCH 032/890] Helper to get current backend instance. Refs #114 --- social/storage/base.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/social/storage/base.py b/social/storage/base.py index 4febce81c..b8d4900b0 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -10,6 +10,7 @@ from openid.association import Association as OpenIdAssociation from social.backends.utils import get_backend +from social.strategies.utils import get_current_strategy CLEAN_USERNAME_REGEX = re.compile(r'[^\w.@+-_]+', re.UNICODE) @@ -21,8 +22,16 @@ class UserMixin(object): uid = None extra_data = None - def get_backend(self, strategy): - return get_backend(strategy.backends, self.provider) + def get_backend(self, strategy=None): + strategy = strategy or get_current_strategy() + if strategy: + return get_backend(strategy.backends, self.provider) + + def get_backend_instance(self, strategy=None): + strategy = strategy or get_current_strategy() + Backend = self.get_backend(strategy) + if Backend: + return Backend(strategy=strategy) @property def tokens(self): From 66354fc14fb95c1a76ca8ee54c00b22f1a3a81aa Mon Sep 17 00:00:00 2001 From: Stephen McDonald Date: Tue, 3 Dec 2013 18:15:11 +1100 Subject: [PATCH 033/890] getpocket.com backend --- docs/backends/pocket.rst | 12 ++++++++++ social/backends/pocket.py | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 docs/backends/pocket.rst create mode 100644 social/backends/pocket.py diff --git a/docs/backends/pocket.rst b/docs/backends/pocket.rst new file mode 100644 index 000000000..e70708560 --- /dev/null +++ b/docs/backends/pocket.rst @@ -0,0 +1,12 @@ +Pocket +====== + +Pocket uses a weird variant of OAuth v2 that only defines a consumer key. + +- Register a new application at the `Pocket API`_, and + +- fill ``consumer key`` value in the settings:: + + SOCIAL_AUTH_POCKET_KEY = '' + +.. _Pocket API: http://getpocket.com/developer/ diff --git a/social/backends/pocket.py b/social/backends/pocket.py new file mode 100644 index 000000000..d991d30db --- /dev/null +++ b/social/backends/pocket.py @@ -0,0 +1,47 @@ +""" +Pocket OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/pocket.html +""" +from social.backends.base import BaseAuth + + +class PocketAuth(BaseAuth): + + name = "pocket" + + AUTHORIZATION_URL = 'https://getpocket.com/auth/authorize' + ACCESS_TOKEN_URL = 'https://getpocket.com/v3/oauth/authorize' + REQUEST_TOKEN_URL = 'https://getpocket.com/v3/oauth/request' + ID_KEY = 'username' + + def get_json(self, url, *args, **kwargs): + headers = {'X-Accept': 'application/json'} + kwargs.update({'method': 'POST', 'headers': headers}) + return super(PocketAuth, self).get_json(url, *args, **kwargs) + + def get_user_details(self, response): + return {"username": response["username"]} + + def extra_data(self, user, uid, response, details): + return response + + def auth_url(self): + data = { + 'consumer_key': self.setting('POCKET_CONSUMER_KEY'), + 'redirect_uri': self.redirect_uri, + } + token = self.get_json(self.REQUEST_TOKEN_URL, data=data)['code'] + self.strategy.session_set('pocket_request_token', token) + bits = (self.AUTHORIZATION_URL, token, self.redirect_uri) + return '%s?request_token=%s&redirect_uri=%s' % bits + + def auth_complete(self, *args, **kwargs): + data = { + 'consumer_key': self.setting('POCKET_CONSUMER_KEY'), + 'code': self.strategy.session_get('pocket_request_token'), + } + response = self.get_json(self.ACCESS_TOKEN_URL, data=data) + username = response['username'] + access_token = response['access_token'] + kwargs.update({'response': response, 'backend': self}) + return self.strategy.authenticate(*args, **kwargs) From f70500ad966887a793ea17ed7b794e5526f828f1 Mon Sep 17 00:00:00 2001 From: Stephen McDonald Date: Tue, 3 Dec 2013 21:28:38 +1100 Subject: [PATCH 034/890] Add refs to getpocket.com in readme + docs --- README.rst | 2 ++ docs/backends/index.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 783635e7c..b04ffb274 100644 --- a/README.rst +++ b/README.rst @@ -83,6 +83,7 @@ or current ones extended): * OpenId_ * OpenSuse_ OpenId http://en.opensuse.org/openSUSE:Connect * Orkut_ OAuth1 + * Pocket_ OAuth2 * Podio_ OAuth2 * Rdio_ OAuth1 and OAuth2 * Readability_ OAuth1 @@ -223,6 +224,7 @@ check `django-social-auth LICENSE`_ for details: .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _Odnoklassniki: http://www.odnoklassniki.ru .. _Orkut: http://www.orkut.com +.. _Pocket: http://getpocket.com .. _Podio: https://podio.com .. _Shopify: http://shopify.com .. _Skyrock: https://skyrock.com diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 787a7da3a..9d4e51b45 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -77,6 +77,7 @@ Social backends mixcloud odnoklassnikiru persona + pocket podio rdio readability From 624eb58f0a423cf7d0afaf2e7d31f05a54f9f7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 3 Dec 2013 14:18:14 -0200 Subject: [PATCH 035/890] PEP8. Refs #116 --- social/backends/pocket.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/social/backends/pocket.py b/social/backends/pocket.py index d991d30db..90bc5a9bf 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -6,9 +6,7 @@ class PocketAuth(BaseAuth): - - name = "pocket" - + name = 'pocket' AUTHORIZATION_URL = 'https://getpocket.com/auth/authorize' ACCESS_TOKEN_URL = 'https://getpocket.com/v3/oauth/authorize' REQUEST_TOKEN_URL = 'https://getpocket.com/v3/oauth/request' @@ -20,7 +18,7 @@ def get_json(self, url, *args, **kwargs): return super(PocketAuth, self).get_json(url, *args, **kwargs) def get_user_details(self, response): - return {"username": response["username"]} + return {'username': response['username']} def extra_data(self, user, uid, response, details): return response @@ -41,7 +39,5 @@ def auth_complete(self, *args, **kwargs): 'code': self.strategy.session_get('pocket_request_token'), } response = self.get_json(self.ACCESS_TOKEN_URL, data=data) - username = response['username'] - access_token = response['access_token'] kwargs.update({'response': response, 'backend': self}) return self.strategy.authenticate(*args, **kwargs) From 5f9b3ea347b8bb92efcd0ffea28b8b043841f735 Mon Sep 17 00:00:00 2001 From: Rodrigue Villetard Date: Fri, 6 Dec 2013 11:11:54 +0100 Subject: [PATCH 036/890] Missing trailing slash on complete url Hi, If the django config attribute APPEND_SLASH is set to false, then the example does not work. Adding the trailing slash solve it. --- docs/backends/persona.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/persona.rst b/docs/backends/persona.rst index 4b831e484..0bfd74bce 100644 --- a/docs/backends/persona.rst +++ b/docs/backends/persona.rst @@ -12,7 +12,7 @@ POST to `python-social-auth`_:: - + Mozilla Persona From 776f5378507285625c76b652bd5e6cc04b5ca7c5 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 6 Dec 2013 10:39:08 -0800 Subject: [PATCH 037/890] Add test backends to the package. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0ea2dfa14..c8ecc6888 100644 --- a/setup.py +++ b/setup.py @@ -55,6 +55,7 @@ def long_description(): 'social.pipeline', 'social.strategies', 'social.tests.actions', + 'social.tests.backends', 'social.tests'], #package_data={'social': ['locale/*/LC_MESSAGES/*']}, long_description=long_description(), From c9713dafcf6e22681bf4720c9395a74c0d71e354 Mon Sep 17 00:00:00 2001 From: monkut Date: Sat, 7 Dec 2013 10:38:27 +0900 Subject: [PATCH 038/890] Removed non-ascii character from author string Trying to install python-social-auth on a system where the default encoding is NOT ascii or latin1, installation fails with UnicodeDecodeError. It seems that the PKG-INFO format requires that header values are in ASCII following RFC-822. (I'm not sure, but it seems that PKG-INFO is auto-generated from this setup.py). My appologies to Matias. C:\Python33\Scripts>python3 pip-3.3-script.py install python-social-auth Downloading/unpacking python-social-auth Downloading python-social-auth-0.1.17.tar.gz (71kB): 71kB downloaded Running setup.py egg_info for package python-social-auth Cleaning up... Exception: Traceback (most recent call last): File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\basecommand.py", line 134, in main status = self.run(options, args) File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\commands\install.py", line 236, in run requirement_set.prepare_files(finder, force_root_egg_info=self.bundle, bundle=self.bundle) File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\req.py", line 1139, in prepare_files req_to_install.assert_source_matches_version() File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\req.py", line 394, in assert_source_matches_version version = self.installed_version File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\req.py", line 390, in installed_version return self.pkg_info()['version'] File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\req.py", line 357, in pkg_info data = self.egg_info_data('PKG-INFO') File "C:\Python33\lib\site-packages\pip-1.4.1-py3.3.egg\pip\req.py", line 297, in egg_info_data data = fp.read() UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 184: illegal multibyte sequence --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0ea2dfa14..66604abc2 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ def long_description(): setup(name='python-social-auth', version=version, - author='Matías Aguirre', + author='Matias Aguirre', author_email='matiasaguirre@gmail.com', description='Python social authentication made simple.', license='BSD', From 58cd182374cf0fbd08604ad36143ef921100bd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Dec 2013 15:16:13 -0200 Subject: [PATCH 039/890] Constant type compare on HMAC signatures. Closes #122 --- social/backends/facebook.py | 4 ++-- social/utils.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 77e5e7717..8f900a523 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -8,7 +8,7 @@ import base64 import hashlib -from social.utils import parse_qs +from social.utils import parse_qs, constant_time_compare from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthException, AuthCanceled, AuthUnknownError @@ -166,6 +166,6 @@ def base64_url_decode(data): expected_sig = hmac.new(secret, msg=payload, digestmod=hashlib.sha256).digest() # allow the signed_request to function for upto 1 day - if sig == expected_sig and \ + if constant_time_compare(sig, expected_sig) and \ data['issued_at'] > (time.time() - 86400): return data diff --git a/social/utils.py b/social/utils.py index 3b1f49c3e..39bc079aa 100644 --- a/social/utils.py +++ b/social/utils.py @@ -1,8 +1,8 @@ import re import sys import unicodedata -import six import collections +import six from social.p3 import urlparse, urlunparse, urlencode, \ parse_qs as battery_parse_qs @@ -143,3 +143,21 @@ def build_absolute_uri(host_url, path=None): if host_url.endswith('/') and path.startswith('/'): path = path[1:] return host_url + path + + +def constant_time_compare(val1, val2): + """ + Returns True if the two strings are equal, False otherwise. + The time taken is independent of the number of characters that match. + This code was borrowed from Django 1.5.4-final + """ + if len(val1) != len(val2): + return False + result = 0 + if six.PY3 and isinstance(val1, bytes) and isinstance(val2, bytes): + for x, y in zip(val1, val2): + result |= x ^ y + else: + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) + return result == 0 From aa85f661799a4073af0b34c1c1b30b622869197d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 9 Dec 2013 14:37:08 -0200 Subject: [PATCH 040/890] Allow unauthorized token retrieval/storage overrideable. Refs #111 --- social/backends/oauth.py | 60 +++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index f965d94ba..46049e6bb 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -87,13 +87,11 @@ class BaseOAuth1(OAuthAuth): OAUTH_TOKEN_PARAMETER_NAME = 'oauth_token' REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' ACCESS_TOKEN_URL = '' + UNATHORIZED_TOKEN_SUFIX = 'unauthorized_token_name' def auth_url(self): """Return redirect url""" - token = self.unauthorized_token() - name = self.name + 'unauthorized_token_name' - tokens = self.strategy.session_get(name, []) + [token] - self.strategy.session_set(name, tokens) + token = self.set_unauthorized_token() return self.oauth_authorization_request(token) def process_error(self, data): @@ -106,27 +104,7 @@ def auth_complete(self, *args, **kwargs): """Return user, might be logged in""" # Multiple unauthorized tokens are supported (see #521) self.process_error(self.data) - name = self.name + 'unauthorized_token_name' - token = None - unauthed_tokens = self.strategy.session_get(name, []) - if not unauthed_tokens: - raise AuthTokenError(self, 'Missing unauthorized token') - token_param_name = self.OAUTH_TOKEN_PARAMETER_NAME - data_token = self.data.get(token_param_name, 'no-token') - for unauthed_token in unauthed_tokens: - orig_unauthed_token = unauthed_token - if not isinstance(unauthed_token, dict): - unauthed_token = parse_qs(unauthed_token) - if unauthed_token.get(token_param_name) == data_token: - self.strategy.session_set(name, list( - set(unauthed_tokens) - - set([orig_unauthed_token])) - ) - token = unauthed_token - break - else: - raise AuthTokenError(self, 'Incorrect tokens') - + token = self.get_unauthorized_token() try: access_token = self.access_token(token) except HTTPError as err: @@ -144,6 +122,38 @@ def do_auth(self, access_token, *args, **kwargs): kwargs.update({'response': data, 'backend': self}) return self.strategy.authenticate(*args, **kwargs) + def get_unauthorized_token(self): + name = self.name + self.UNATHORIZED_TOKEN_SUFIX + unauthed_tokens = self.strategy.session_get(name, []) + if not unauthed_tokens: + raise AuthTokenError(self, 'Missing unauthorized token') + + data_token = self.data.get(self.OAUTH_TOKEN_PARAMETER_NAME) + + if data_token is None: + raise AuthTokenError(self, 'Missing unauthorized token') + + token = None + for utoken in unauthed_tokens: + orig_utoken = utoken + if not isinstance(utoken, dict): + utoken = parse_qs(utoken) + if utoken.get(self.OAUTH_TOKEN_PARAMETER_NAME) == data_token: + self.strategy.session_set(name, list(set(unauthed_tokens) - + set([orig_utoken]))) + token = utoken + break + else: + raise AuthTokenError(self, 'Incorrect tokens') + return token + + def set_unauthorized_token(self): + token = self.unauthorized_token() + name = self.name + self.UNATHORIZED_TOKEN_SUFIX + tokens = self.strategy.session_get(name, []) + [token] + self.strategy.session_set(name, tokens) + return token + def unauthorized_token(self): """Return request for unauthorized token (first stage)""" params = self.request_token_extra_arguments() From 3131d77dfcb6505c11376966bce830129065586c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 9 Dec 2013 22:51:56 -0200 Subject: [PATCH 041/890] Avoid broken email entries on yahoo API. Closes #125 --- social/backends/yahoo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 60356b7bf..4f0873e52 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -30,12 +30,11 @@ def get_user_details(self, response): """Return user details from Yahoo Profile""" fname = response.get('givenName') lname = response.get('familyName') - if 'emails' in response: - email = response.get('emails')[0]['handle'] - else: - email = '' + emails = [email for email in response.get('emails', []) + if email.get('handle')] + emails.sort(key=lambda e: e.get('primary', False)) return {'username': response.get('nickname'), - 'email': email, + 'email': emails[0]['handle'] if emails else '', 'fullname': '{0} {1}'.format(fname, lname), 'first_name': fname, 'last_name': lname} From 093fe17e3f62b8cd92f75334765801e567e1affc Mon Sep 17 00:00:00 2001 From: Bob Alcorn Date: Wed, 11 Dec 2013 12:37:37 -0500 Subject: [PATCH 042/890] Updated pipeline example to include externalized auth; See http://stackoverflow.com/questions/20504515/associate-only-authentication-with-python-social-authentication-and-django --- docs/pipeline.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index eff34b735..68bca16a4 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -52,6 +52,19 @@ ones would look like this:: 'social.pipeline.user.user_details' ) +Note that this assumes the user is already authenticated, and thus the ``user`` key +in the dict is populated. In cases where the authentication is purely external, a +pipeline method must be provided that populates the ``user`` key. Example:: + + + SOCIAL_AUTH_PIPELINE = ( + 'myapp.pipeline.load_user', + 'social.pipeline.social_auth.social_user', + 'social.pipeline.social_auth.associate_user', + 'social.pipeline.social_auth.load_extra_data', + 'social.pipeline.user.user_details', + ) + Each pipeline function will receive the following parameters: * Current strategy (which gives access to current store, backend and request) * User ID given by authentication provider From 0220ae249f0243ab056ef927c964d80de278243e Mon Sep 17 00:00:00 2001 From: Kevin Tran Date: Wed, 11 Dec 2013 16:53:41 -0800 Subject: [PATCH 043/890] Added support for named URLs and URL translation using the django built-in resolve_url before giving the url to tje HttpResponseRedirect. See also https://code.djangoproject.com/ticket/15552 --- social/strategies/django_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 26c4f554a..a7ac7e569 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -3,6 +3,7 @@ from django.db.models import Model from django.contrib.contenttypes.models import ContentType from django.contrib.auth import authenticate +from django.shortcuts import redirect from django.template import TemplateDoesNotExist, RequestContext, loader from django.utils.datastructures import MergeDict from django.utils.translation import get_language @@ -48,7 +49,7 @@ def request_host(self): return self.request.get_host() def redirect(self, url): - return HttpResponseRedirect(url) + return redirect(url) def html(self, content): return HttpResponse(content, content_type='text/html;charset=UTF-8') From 3c3695d847862fdfca5e3ea8e3e095b7f59d40bc Mon Sep 17 00:00:00 2001 From: maxtepkeev Date: Mon, 16 Dec 2013 15:12:11 +0300 Subject: [PATCH 044/890] fix session expiration in vk backend vk backend uses expires_in param and not expires with access token expiration information --- social/backends/vk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/vk.py b/social/backends/vk.py index 2f7bbcd29..c244bd9f1 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -80,7 +80,7 @@ class VKOAuth2(BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' EXTRA_DATA = [ ('id', 'id'), - ('expires', 'expires') + ('expires_in', 'expires') ] def get_user_details(self, response): From 23211f77f62519cf62c65b940cb7697c36a0c82a Mon Sep 17 00:00:00 2001 From: Jay Parlar Date: Tue, 17 Dec 2013 11:49:15 -0500 Subject: [PATCH 045/890] Tiny typo fix --- docs/configuration/django.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index e37a75470..45c90722d 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -41,7 +41,7 @@ Sync database to create needed models:: ./manage.py syncdb -Autentication backends +Authentication backends ---------------------- Add desired authentication backends to Django's AUTHENTICATION_BACKENDS_ From 1588c87de05cd952bb8d776228e63620c5ba4eac Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Wed, 25 Dec 2013 22:52:52 -0800 Subject: [PATCH 046/890] Update reddit.py --- social/backends/reddit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/reddit.py b/social/backends/reddit.py index 3024f1134..96be7f811 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -26,7 +26,7 @@ class RedditOAuth2(BaseOAuth2): ] def get_user_details(self, response): - """Return user details from Github account""" + """Return user details from Reddit account""" return {'username': response.get('name'), 'email': '', 'fullname': '', 'first_name': '', 'last_name': ''} From d208d71d4f170b89e204c3594bfa8e7b3aeeeeff Mon Sep 17 00:00:00 2001 From: Nicolas Cortot Date: Thu, 26 Dec 2013 11:18:33 +0100 Subject: [PATCH 047/890] Support for MongoEngine authentication using Custom User Model --- social/apps/django_app/me/models.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/social/apps/django_app/me/models.py b/social/apps/django_app/me/models.py index 1314a3a71..e530ed197 100644 --- a/social/apps/django_app/me/models.py +++ b/social/apps/django_app/me/models.py @@ -22,11 +22,26 @@ UNUSABLE_PASSWORD = '!' # Borrowed from django 1.4 -USER_MODEL = module_member( - getattr(settings, setting_name('USER_MODEL'), None) or - getattr(settings, 'AUTH_USER_MODEL', None) or - 'mongoengine.django.auth.User' -) +def _get_user_model(): + """Get the User Document class user for MongoEngine authentication. + + Use the model defined in SOCIAL_AUTH_USER_MODEL if defined, or + defaults to MongoEngine's configured user document class. + + """ + + custom_model = getattr(settings, setting_name('USER_MODEL'), None) + if custom_model: + return module_member(custom_model) + try: + # Custom user model support with MongoEngine 0.8 + from mongoengine.django.mongo_auth.models import get_user_document + return get_user_document() + except ImportError: + return module_member('mongoengine.django.auth.User') + + +USER_MODEL = _get_user_model() class UserSocialAuth(Document, DjangoUserMixin): From 5b42cf27eba03e9e56e40cfa4b3e8c82b334a4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 27 Dec 2013 00:08:33 -0200 Subject: [PATCH 048/890] PEP8 --- social/apps/django_app/me/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/social/apps/django_app/me/models.py b/social/apps/django_app/me/models.py index e530ed197..de70fe78c 100644 --- a/social/apps/django_app/me/models.py +++ b/social/apps/django_app/me/models.py @@ -23,16 +23,16 @@ def _get_user_model(): - """Get the User Document class user for MongoEngine authentication. + """ + Get the User Document class user for MongoEngine authentication. Use the model defined in SOCIAL_AUTH_USER_MODEL if defined, or defaults to MongoEngine's configured user document class. - """ - custom_model = getattr(settings, setting_name('USER_MODEL'), None) if custom_model: return module_member(custom_model) + try: # Custom user model support with MongoEngine 0.8 from mongoengine.django.mongo_auth.models import get_user_document From 17ce485e8fbffcad0cde30a8184afe9f5258aae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 27 Dec 2013 11:07:07 -0200 Subject: [PATCH 049/890] Update porting docs regarding session value --- docs/configuration/porting_from_dsa.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/configuration/porting_from_dsa.rst b/docs/configuration/porting_from_dsa.rst index dbe350fa7..6254cb2b6 100644 --- a/docs/configuration/porting_from_dsa.rst +++ b/docs/configuration/porting_from_dsa.rst @@ -103,6 +103,14 @@ changes. Examples of the new import paths:: ) +Session +------- + +Django stores the last authentication backend used in the user session, this +can cause import troubles when porting since the old import paths aren't valid +anymore. Sadly so far the only solution is to clean the sessions content, that +means to force the user to login again. + .. _django-social-auth: https://github.com/omab/django-social-auth .. _python-social-auth: https://github.com/omab/python-social-auth .. _example app: https://github.com/omab/python-social-auth/blob/master/examples/django_example/dj/urls.py#L7 From 583245414433f08d809661d871b5615c75b2a134 Mon Sep 17 00:00:00 2001 From: Xmypblu Date: Sat, 28 Dec 2013 03:13:47 +0400 Subject: [PATCH 050/890] Add support for OpenStreetMap OAuth --- README.rst | 2 + docs/backends/index.rst | 1 + docs/backends/openstreetmap.rst | 18 +++++++ examples/django_example/example/app/views.py | 7 +++ examples/django_example/example/settings.py | 1 + .../example/templates/done.html | 2 +- .../example/templates/home.html | 1 + examples/django_example/example/urls.py | 1 + social/backends/openstreetmap.py | 51 +++++++++++++++++++ 9 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 docs/backends/openstreetmap.rst create mode 100644 social/backends/openstreetmap.py diff --git a/README.rst b/README.rst index b04ffb274..b13503fcd 100644 --- a/README.rst +++ b/README.rst @@ -81,6 +81,7 @@ or current ones extended): * `Mozilla Persona`_ * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ + * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth * OpenSuse_ OpenId http://en.opensuse.org/openSUSE:Connect * Orkut_ OAuth1 * Pocket_ OAuth2 @@ -264,3 +265,4 @@ check `django-social-auth LICENSE`_ for details: .. _python-oauth2: https://github.com/simplegeo/python-oauth2 .. _sqlalchemy: http://www.sqlalchemy.org/ .. _pypi: http://pypi.python.org/pypi/python-social-auth/ +.. _OpenStreetMap: http://www.openstreetmap.org \ No newline at end of file diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 9d4e51b45..8f346d6af 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -76,6 +76,7 @@ Social backends mendeley mixcloud odnoklassnikiru + openstreetmap persona pocket podio diff --git a/docs/backends/openstreetmap.rst b/docs/backends/openstreetmap.rst new file mode 100644 index 000000000..1c4ae3393 --- /dev/null +++ b/docs/backends/openstreetmap.rst @@ -0,0 +1,18 @@ +OpenStreetMap +============= + +OpenStreetMap supports OAuth 1.0 and 1.0a but 1.0a should be used for the new applications, as 1.0 is for support of legacy clients only. +Access tokens currently do not expire automatically. +More documentation at `OpenStreetMap Wiki`_: + +- Login to your account + +- Register your application as OAuth consumer on your `OpenStreetMap user settings page`_, and + +- Set ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_OPENSTREETMAP_KEY = '' + SOCIAL_AUTH_OPENSTREETMAP_SECRET = '' + +.. _OpenStreetMap Wiki: http://wiki.openstreetmap.org/wiki/OAuth +.. _OpenStreetMap user settings page: http://www.openstreetmap.org/user/username/oauth_clients/new diff --git a/examples/django_example/example/app/views.py b/examples/django_example/example/app/views.py index 496985fbf..aca7c024a 100644 --- a/examples/django_example/example/app/views.py +++ b/examples/django_example/example/app/views.py @@ -2,10 +2,17 @@ from django.template import RequestContext from django.shortcuts import render_to_response, redirect from django.contrib.auth.decorators import login_required +from django.contrib.auth import logout as auth_logout from social.backends.google import GooglePlusAuth +def logout(request): + """Logs out user""" + auth_logout(request) + return render_to_response('home.html', {}, RequestContext(request)) + + def home(request): """Home view, displays login mechanism""" if request.user.is_authenticated(): diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index d0087ff80..a5f49b513 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -155,6 +155,7 @@ 'social.backends.mixcloud.MixcloudOAuth2', 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', 'social.backends.open_id.OpenIdAuth', + 'social.backends.openstreetmap.OpenStreetMapOAuth', 'social.backends.orkut.OrkutOAuth', 'social.backends.persona.PersonaAuth', 'social.backends.podio.PodioOAuth2', diff --git a/examples/django_example/example/templates/done.html b/examples/django_example/example/templates/done.html index 09cd9bbbf..dfce6fbdb 100644 --- a/examples/django_example/example/templates/done.html +++ b/examples/django_example/example/templates/done.html @@ -8,7 +8,7 @@
    {% for assoc in backends.associated %}
  • - {{ assoc.provider }} (Disconnect) + {{ assoc.provider }} (Disconnect or logout)
  • {% endfor %}
diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 9f2ccc26e..ec6b6d553 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -34,6 +34,7 @@ Mendeley OAuth1
Mixcloud OAuth2
Odnoklassniki OAuth2
+OpenStreetMap OAuth1
OpenSUSE OpenId
Orkut OAuth
Podio OAuth2
diff --git a/examples/django_example/example/urls.py b/examples/django_example/example/urls.py index 9821119bc..2a562ab23 100644 --- a/examples/django_example/example/urls.py +++ b/examples/django_example/example/urls.py @@ -10,6 +10,7 @@ url(r'^signup-email/', 'example.app.views.signup_email'), url(r'^email-sent/', 'example.app.views.validation_sent'), url(r'^login/$', 'example.app.views.home'), + url(r'^logout/$', 'example.app.views.logout'), url(r'^done/$', 'example.app.views.done', name='done'), url(r'^email/$', 'example.app.views.require_email', name='require_email'), url(r'', include('social.apps.django_app.urls', namespace='social')) diff --git a/social/backends/openstreetmap.py b/social/backends/openstreetmap.py new file mode 100644 index 000000000..0207760e6 --- /dev/null +++ b/social/backends/openstreetmap.py @@ -0,0 +1,51 @@ +""" +OpenStreetMap OAuth support. + +This adds support for OpenStreetMap OAuth service. An application must be +registered first on OpenStreetMap and the settings +SOCIAL_AUTH_OPENSTREETMAP_KEY and SOCIAL_AUTH_OPENSTREETMAP_SECRET +must be defined with the corresponding values. + +More info: http://wiki.openstreetmap.org/wiki/OAuth +""" + +from xml.dom import minidom +from social.backends.oauth import BaseOAuth1 + +class OpenStreetMapOAuth(BaseOAuth1): + """OpenStreetMap OAuth authentication backend""" + name = 'openstreetmap' + AUTHORIZATION_URL = 'http://www.openstreetmap.org/oauth/authorize' + REQUEST_TOKEN_URL = 'http://www.openstreetmap.org/oauth/request_token' + ACCESS_TOKEN_URL = 'http://www.openstreetmap.org/oauth/access_token' + + EXTRA_DATA = [ + ('id','id'), + ('avatar','avatar'), + ('account_created','account_created') + ] + + def get_user_details(self, response): + """Return user details from OpenStreetMap account""" + return { + 'username': response['username'], + 'email': '', + 'fullname': '', + 'first_name': '', + 'last_name': '' + } + + def user_data(self, access_token, *args, **kwargs): + """Return user data provided""" + url = 'http://api.openstreetmap.org/api/0.6/user/details' + try: + dom = minidom.parseString(self.oauth_request(access_token,url).content) + except ValueError: + return None + + return { + 'id': dom.getElementsByTagName('user')[0].getAttribute('id'), + 'username': dom.getElementsByTagName('user')[0].getAttribute('display_name'), + 'avatar': dom.getElementsByTagName('img')[0].getAttribute('href'), + 'account_created': dom.getElementsByTagName('user')[0].getAttribute('account_created') + } From a013d43dabe643173b75e15ed64dd48634275d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 28 Dec 2013 02:45:25 -0200 Subject: [PATCH 051/890] PEP8. Refs #135 --- social/backends/openstreetmap.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/social/backends/openstreetmap.py b/social/backends/openstreetmap.py index 0207760e6..46e9cd912 100644 --- a/social/backends/openstreetmap.py +++ b/social/backends/openstreetmap.py @@ -8,22 +8,22 @@ More info: http://wiki.openstreetmap.org/wiki/OAuth """ - from xml.dom import minidom + from social.backends.oauth import BaseOAuth1 + class OpenStreetMapOAuth(BaseOAuth1): """OpenStreetMap OAuth authentication backend""" name = 'openstreetmap' AUTHORIZATION_URL = 'http://www.openstreetmap.org/oauth/authorize' REQUEST_TOKEN_URL = 'http://www.openstreetmap.org/oauth/request_token' ACCESS_TOKEN_URL = 'http://www.openstreetmap.org/oauth/access_token' - EXTRA_DATA = [ - ('id','id'), - ('avatar','avatar'), - ('account_created','account_created') - ] + ('id', 'id'), + ('avatar', 'avatar'), + ('account_created', 'account_created') + ] def get_user_details(self, response): """Return user details from OpenStreetMap account""" @@ -33,19 +33,21 @@ def get_user_details(self, response): 'fullname': '', 'first_name': '', 'last_name': '' - } + } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" - url = 'http://api.openstreetmap.org/api/0.6/user/details' + response = self.oauth_request( + access_token, 'http://api.openstreetmap.org/api/0.6/user/details' + ) try: - dom = minidom.parseString(self.oauth_request(access_token,url).content) + dom = minidom.parseString(response.content) except ValueError: return None - + user = dom.getElementsByTagName('user')[0] return { - 'id': dom.getElementsByTagName('user')[0].getAttribute('id'), - 'username': dom.getElementsByTagName('user')[0].getAttribute('display_name'), - 'avatar': dom.getElementsByTagName('img')[0].getAttribute('href'), - 'account_created': dom.getElementsByTagName('user')[0].getAttribute('account_created') - } + 'id': user.getAttribute('id'), + 'username': user.getAttribute('display_name'), + 'account_created': user.getAttribute('account_created'), + 'avatar': dom.getElementsByTagName('img')[0].getAttribute('href') + } From 870105d08095025f91efd33f38f2def7984a2bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 28 Dec 2013 02:46:03 -0200 Subject: [PATCH 052/890] Line chars limit in docs. Refs #135 --- docs/backends/openstreetmap.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/backends/openstreetmap.rst b/docs/backends/openstreetmap.rst index 1c4ae3393..b837d8d29 100644 --- a/docs/backends/openstreetmap.rst +++ b/docs/backends/openstreetmap.rst @@ -1,8 +1,11 @@ OpenStreetMap ============= -OpenStreetMap supports OAuth 1.0 and 1.0a but 1.0a should be used for the new applications, as 1.0 is for support of legacy clients only. +OpenStreetMap supports OAuth 1.0 and 1.0a but 1.0a should be used for the new +applications, as 1.0 is for support of legacy clients only. + Access tokens currently do not expire automatically. + More documentation at `OpenStreetMap Wiki`_: - Login to your account From 2b17622084e8de4d17ec1e0ea132b6d7de4d0689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 1 Jan 2014 14:13:52 -0200 Subject: [PATCH 053/890] Fix docstring. Refs #136 --- social/backends/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/base.py b/social/backends/base.py index 808935ddb..f589a963a 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -117,7 +117,8 @@ def auth_allowed(self, response, details): return allowed def get_user_id(self, details, response): - """Must return a unique ID from values returned on details""" + """Return a unique ID for the current user, by default from server + response.""" return response.get(self.ID_KEY) def get_user_details(self, response): From 20eb2271bef1709cd1cad4f8597f61b11b1ae15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 2 Jan 2014 14:18:45 -0200 Subject: [PATCH 054/890] Use cases doc. Refs #137 --- docs/index.rst | 1 + docs/use_cases.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/use_cases.rst diff --git a/docs/index.rst b/docs/index.rst index 3218bacfd..e585355c7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,6 +27,7 @@ Contents: exceptions backends/index tests + use_cases thanks copyright diff --git a/docs/use_cases.rst b/docs/use_cases.rst new file mode 100644 index 000000000..28faf59d0 --- /dev/null +++ b/docs/use_cases.rst @@ -0,0 +1,22 @@ +Use Cases +========= + +Some miscellaneous options and use cases for python-social-auth_. + + +Return the user to the original page +------------------------------------ + +There's a common scenario were it's desired to return the user back to the +original page from where it was requested to login. For that purpose, the usual +``next`` query-string argument is used, the value of this parameter will be +stored in the session and later used to redirect the user when login was +successful. + +In order to use it just define it with your link, for instance, when using +Django:: + + Login with Facebook + + +.. _python-social-auth: https://github.com/omab/python-social-auth From 87a4943e3486700270273569ad05823e59c65beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 2 Jan 2014 14:25:59 -0200 Subject: [PATCH 055/890] Fix ID_KEY for Tumblr backend. Refs #136 --- social/backends/tumblr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/tumblr.py b/social/backends/tumblr.py index 2e62a37ff..9bfd8261d 100644 --- a/social/backends/tumblr.py +++ b/social/backends/tumblr.py @@ -8,7 +8,7 @@ class TumblrOAuth(BaseOAuth1): name = 'tumblr' - ID_KEY = 'username' + ID_KEY = 'name' AUTHORIZATION_URL = 'http://www.tumblr.com/oauth/authorize' REQUEST_TOKEN_URL = 'http://www.tumblr.com/oauth/request_token' REQUEST_TOKEN_METHOD = 'POST' From 7075a4884111687458c0c42f079a66e5632bd168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Jan 2014 18:08:18 -0200 Subject: [PATCH 056/890] Move extra-data logic to base clase --- docs/backends/implementation.rst | 42 ++++++++++++++++++++++++-------- social/backends/base.py | 22 +++++++++++++++-- social/backends/email.py | 1 + social/backends/oauth.py | 26 ++------------------ social/backends/username.py | 1 + 5 files changed, 56 insertions(+), 36 deletions(-) diff --git a/docs/backends/implementation.rst b/docs/backends/implementation.rst index b2429d87a..2561dd2f6 100644 --- a/docs/backends/implementation.rst +++ b/docs/backends/implementation.rst @@ -6,6 +6,38 @@ settings and methods overrides to retrieve user data from services API. Follow the details below. +Common attributes +----------------- + +First, lets check the common attributes for all backend types. + +``name = ''`` + Any backend needs a name, usually the popular name of the service is used, + like ``facebook``, ``twitter``, etc. It must be unique, otherwise another + backend can take precedence if it's listed before in + ``AUTHENTICATION_BACKENDS`` setting. + +``ID_KEY = None`` + Defines the attribute in the service response that identifies the user as + unique in the service, the value is later stored in the ``uid`` attribute + in the ``UserSocialAuth`` instance. + +``REQUIRES_EMAIL_VALIDATION = False`` + Flags the backend to enforce email validation during the pipeline (if the + corresponding pipeline ``social.pipeline.mail.mail_validation`` was + enabled). + +``EXTRA_DATA = None`` + During the auth process some basic user data is returned by the provider or + retrieved by ``user_data()`` method which usually is used to call some API + on the provider to retrieve it. This data will be stored under + ``UserSocialAuth.extra_data`` attribute, but to make it accessible under + some common names on different providers, this attribute defines a list of + tuples in the form ``(name, alias)`` where ``name`` is the key in the user + data (which should be a ``dict`` instance) and ``alias`` is the name to + store it on ``extra_data``. + + OAuth ----- @@ -46,16 +78,6 @@ Shared attributes from provider to provider, override the default value with this attribute if it differs. -``EXTRA_DATA = None`` - During the auth process some basic user data is returned by the provider or - retrieved by the ``user_data()`` method which calls some API on the - provider to retrieve it. This data will be stored under - ``UserSocialAuth.extra_data`` attribute, but to make it accessible under - some common names on different providers, this attribute defines a list of - tuples in the form ``(name, alias)`` where ``name`` is the key in the user - data (which should be a ``dict`` instance) and ``alias`` is the name to - store it on ``extra_data``. - OAuth2 ****** diff --git a/social/backends/base.py b/social/backends/base.py index f589a963a..ab9a033a7 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -9,6 +9,7 @@ class BaseAuth(object): name = '' # provider name, it's stored in database supports_inactive_user = False # Django auth ID_KEY = None + EXTRA_DATA = None REQUIRES_EMAIL_VALIDATION = False def __init__(self, strategy=None, redirect_uri=None, *args, **kwargs): @@ -101,8 +102,25 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): return out def extra_data(self, user, uid, response, details): - """Return default blank user extra data""" - return {} + """Return deafault extra data to store in extra_data field""" + data = {} + for entry in (self.EXTRA_DATA or []) + self.setting('EXTRA_DATA', []): + if not isinstance(entry, (list, tuple)): + entry = (entry,) + size = len(entry) + if size >= 1 and size <= 3: + if size == 3: + name, alias, discard = entry + elif size == 2: + (name, alias), discard = entry, False + elif size == 1: + name = alias = entry[0] + discard = False + value = response.get(name) or details.get(name) + if discard and not value: + continue + data[alias] = value + return data def auth_allowed(self, response, details): """Return True if the user should be allowed to authenticate, by diff --git a/social/backends/email.py b/social/backends/email.py index 4666499cb..b70f1ea67 100644 --- a/social/backends/email.py +++ b/social/backends/email.py @@ -9,3 +9,4 @@ class EmailAuth(LegacyAuth): name = 'email' ID_KEY = 'email' REQUIRES_EMAIL_VALIDATION = True + EXTRA_DATA = ['email'] diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 46049e6bb..e889546b5 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -15,10 +15,6 @@ class OAuthAuth(BaseAuth): """OAuth authentication backend base class. - EXTRA_DATA defines a set of name that will be stored in - extra_data field. It must be a list of tuples with - name and alias. - Also settings will be inspected to get more values names that should be stored on extra_data field. Setting name is created from current backend name (all uppercase) plus _EXTRA_DATA. @@ -28,32 +24,14 @@ class OAuthAuth(BaseAuth): SCOPE_PARAMETER_NAME = 'scope' DEFAULT_SCOPE = None SCOPE_SEPARATOR = ' ' - EXTRA_DATA = None ID_KEY = 'id' ACCESS_TOKEN_METHOD = 'GET' def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in extra_data field""" - data = {'access_token': response.get('access_token', '')} - names = (self.EXTRA_DATA or []) + \ - self.setting('EXTRA_DATA', []) - for entry in names: - if not isinstance(entry, (list, tuple)): - entry = (entry,) - size = len(entry) - if size >= 1 and size <= 3: - if size == 3: - name, alias, discard = entry - elif size == 2: - (name, alias), discard = entry, False - elif size == 1: - name = alias = entry[0] - discard = False - value = response.get(name) - if discard and not value: - continue - data[alias] = value + data = super(OAuthAuth, self).extra_data(user, uid, response, details) + data['access_token'] = response.get('access_token', '') return data def get_scope(self): diff --git a/social/backends/username.py b/social/backends/username.py index d519cfe43..c88da09a5 100644 --- a/social/backends/username.py +++ b/social/backends/username.py @@ -8,3 +8,4 @@ class UsernameAuth(LegacyAuth): name = 'username' ID_KEY = 'username' + EXTRA_DATA = ['username'] From dd447cdcb9f6fd1282ff2474f5d4bc08757aff79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Jan 2014 18:16:20 -0200 Subject: [PATCH 057/890] Always send email validations is required --- social/pipeline/mail.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index 44c203a05..73dd3599e 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -5,15 +5,16 @@ @partial def mail_validation(strategy, details, user=None, is_new=False, *args, **kwargs): - if user is None or is_new and details.get('email'): + requires_validation = strategy.backend.REQUIRES_EMAIL_VALIDATION or \ + strategy.setting('FORCE_EMAIL_VALIDATION', False) + if requires_validation and details.get('email'): data = strategy.request_data() if 'verification_code' in data: strategy.session_pop('email_validation_address') if not strategy.validate_email(details['email'], data['verification_code']): raise InvalidEmail(strategy.backend) - elif strategy.backend.REQUIRES_EMAIL_VALIDATION or \ - strategy.setting('FORCE_EMAIL_VALIDATION', False): + else: strategy.send_email_validation(details['email']) strategy.session_set('email_validation_address', details['email']) return strategy.redirect(strategy.setting('EMAIL_VALIDATION_URL')) From b94b41fc37c3a7bd72aa5dc1e8f373418228b05d Mon Sep 17 00:00:00 2001 From: Jichao Ouyang Date: Mon, 6 Jan 2014 23:01:59 +0800 Subject: [PATCH 058/890] add support for taobao --- social/backends/taobao.py | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 social/backends/taobao.py diff --git a/social/backends/taobao.py b/social/backends/taobao.py new file mode 100644 index 000000000..21e2992a6 --- /dev/null +++ b/social/backends/taobao.py @@ -0,0 +1,67 @@ +import urllib2,urllib +from urllib2 import Request, urlopen, HTTPError +from urllib import urlencode +from urlparse import urlsplit +from social_auth.backends.exceptions import StopPipeline, AuthException, \ + AuthFailed, AuthCanceled, \ + AuthUnknownError, AuthTokenError, \ + AuthMissingParameter +from django.conf import settings +from django.utils import simplejson +import json +from django.contrib.auth import authenticate +from social_auth.backends import BaseOAuth2, OAuthBackend, USERNAME +# taobao OAuth base configuration +TAOBAO_OAUTH_HOST = 'oauth.taobao.com' +# TAOBAO_OAUTH_ROOT = 'authorize' +#Always use secure connection +TAOBAO_OAUTH_REQUEST_TOKEN_URL = 'https://%s/request_token' % (TAOBAO_OAUTH_HOST) +TAOBAO_OAUTH_AUTHORIZATION_URL = 'https://%s/authorize' % (TAOBAO_OAUTH_HOST) +TAOBAO_OAUTH_ACCESS_TOKEN_URL = 'https://%s/token' % (TAOBAO_OAUTH_HOST) + +TAOBAO_CHECK_AUTH = 'https://eco.taobao.com/router/rest' +TAOBAO_USER_SHOW = 'https://%s/user/get_user_info' % TAOBAO_OAUTH_HOST +class TAOBAOBackend(OAuthBackend): + """Taobao OAuth authentication backend""" + name = 'taobao' + + def get_user_id(self, details, response): + return response['taobao_user_id'] + + def get_user_details(self, response): + """Return user details from Taobao account""" + + username = response['taobao_user_nick'] + return {USERNAME: username, + 'email': '', # not supplied + 'last_name': ''} + +class TAOBAOAuth(BaseOAuth2): + """Taobao OAuth authentication mechanism""" + AUTHORIZATION_URL = TAOBAO_OAUTH_AUTHORIZATION_URL + REQUEST_TOKEN_URL = TAOBAO_OAUTH_REQUEST_TOKEN_URL + ACCESS_TOKEN_URL = TAOBAO_OAUTH_ACCESS_TOKEN_URL + SERVER_URL = TAOBAO_OAUTH_HOST + AUTH_BACKEND = TAOBAOBackend + SETTINGS_KEY_NAME = 'TAOBAO_CONSUMER_KEY' + SETTINGS_SECRET_NAME = 'TAOBAO_CONSUMER_SECRET' + + def user_data(self, access_token): + """Return user data provided""" + params = {'method':'taobao.user.get', + 'fomate':'json', + 'v':'2.0', + 'access_token': access_token} + + url = TAOBAO_CHECK_AUTH + urllib.urlencode(params) + # # print url + + try: + return simplejson.load(urllib.urlopen(url)) + except ValueError: + return None + +# Backend definition +BACKENDS = { + 'taobao': TAOBAOAuth, +} From 60f975156ae679e86c52deaa736afd8f2b58ebd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 6 Jan 2014 14:21:38 -0200 Subject: [PATCH 059/890] Updated readme with other dependencies. Closes #140 --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b13503fcd..93817f11d 100644 --- a/README.rst +++ b/README.rst @@ -143,6 +143,10 @@ Dependencies that **must** be met to use the application: - Several backends demand application registration on their corresponding sites and other dependencies like sqlalchemy_ on Flask and Webpy. +- Other dependencies: + * six_ + * requests_ + Documents ========= @@ -265,4 +269,6 @@ check `django-social-auth LICENSE`_ for details: .. _python-oauth2: https://github.com/simplegeo/python-oauth2 .. _sqlalchemy: http://www.sqlalchemy.org/ .. _pypi: http://pypi.python.org/pypi/python-social-auth/ -.. _OpenStreetMap: http://www.openstreetmap.org \ No newline at end of file +.. _OpenStreetMap: http://www.openstreetmap.org +.. _six: http://pythonhosted.org/six/ +.. _requests: http://docs.python-requests.org/en/latest/ From 72a2ad777be4f2244bc42b333f369d8e757f6c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 6 Jan 2014 15:32:22 -0200 Subject: [PATCH 060/890] Simplify partial handling on actions --- social/actions.py | 44 ++++++++++++++++---------------------- social/tests/test_utils.py | 15 +++++++++---- social/utils.py | 14 +++++++----- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/social/actions.py b/social/actions.py index 367a64dd2..ee65b54a5 100644 --- a/social/actions.py +++ b/social/actions.py @@ -40,15 +40,9 @@ def do_complete(strategy, login, user=None, redirect_name='next', strategy.setting('LOGIN_URL') partial = partial_pipeline_data(strategy, user, *args, **kwargs) - if partial is not None: - idx, backend, xargs, xkwargs = partial - if backend == strategy.backend.name: - user = strategy.continue_pipeline(pipeline_index=idx, - *xargs, **xkwargs) - else: - strategy.clean_partial_pipeline() - user = strategy.complete(user=user, request=strategy.request, - *args, **kwargs) + if partial: + xargs, xkwargs = partial + user = strategy.continue_pipeline(*xargs, **xkwargs) else: user = strategy.complete(user=user, request=strategy.request, *args, **kwargs) @@ -98,21 +92,19 @@ def do_complete(strategy, login, user=None, redirect_name='next', def do_disconnect(strategy, user, association_id=None, redirect_name='next', *args, **kwargs): partial = partial_pipeline_data(strategy, user, *args, **kwargs) - out = None - if partial is not None: - idx, backend, xargs, xkwargs = partial - if backend == strategy.backend.name: - out = strategy.disconnect(pipeline_index=idx, user=user, - association_id=association_id, - *args, **kwargs) - if out is None: - strategy.clean_partial_pipeline() - out = strategy.disconnect(user=user, association_id=association_id, - *args, **kwargs) - if not isinstance(out, dict): - return out + if partial: + xargs, xkwargs = partial + response = strategy.disconnect(association_id=association_id, + *xargs, **xkwargs) else: - data = strategy.request_data() - return strategy.redirect(data.get(redirect_name, '') or - strategy.setting('DISCONNECT_REDIRECT_URL') or - strategy.setting('LOGIN_REDIRECT_URL')) + response = strategy.disconnect(user=user, + association_id=association_id, + *args, **kwargs) + + if isinstance(response, dict): + response = strategy.redirect( + strategy.request_data().get(redirect_name, '') or + strategy.setting('DISCONNECT_REDIRECT_URL') or + strategy.setting('LOGIN_REDIRECT_URL') + ) + return response diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index 373b668c7..42020c2e6 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -125,14 +125,21 @@ class PartialPipelineData(unittest.TestCase): class MockStrategy(object): request = None + def __init__(self, *args, **kwargs): + class MockBackend(object): + name = 'mock-backend' + self.backend = MockBackend() + def session_get(self, name, default=None): - return object() + return { + 'partial_pipeline': (0, 'mock-backend', [], {}) + }.get(name) def partial_from_session(self, session): - return object(), object(), [], {} + return session def test_kwargs_included_in_result(self): kwargitem = ('foo', 'bar') - _, _, _, xkwargs = partial_pipeline_data(self.MockStrategy(), None, - **dict([kwargitem])) + _, xkwargs = partial_pipeline_data(self.MockStrategy(), None, + *(), **dict([kwargitem])) xkwargs.should.have.key(kwargitem[0]).being.equal(kwargitem[1]) diff --git a/social/utils.py b/social/utils.py index 39bc079aa..87666c400 100644 --- a/social/utils.py +++ b/social/utils.py @@ -128,11 +128,15 @@ def partial_pipeline_data(strategy, user, *args, **kwargs): partial = strategy.session_get('partial_pipeline', None) if partial: idx, backend, xargs, xkwargs = strategy.partial_from_session(partial) - kwargs = kwargs.copy() - kwargs.setdefault('user', user) - kwargs.setdefault('request', strategy.request) - kwargs.update(xkwargs) - return idx, backend, xargs, kwargs + if backend == strategy.backend.name: + kwargs = kwargs.copy() + kwargs.setdefault('pipeline_index', idx) + kwargs.setdefault('user', user) + kwargs.setdefault('request', strategy.request) + kwargs.update(xkwargs) + return xargs, kwargs + else: + strategy.clean_partial_pipeline() def build_absolute_uri(host_url, path=None): From 0d5d013afb10cca45a23ca8f842e10a96a20dc95 Mon Sep 17 00:00:00 2001 From: Edwin Knuth Date: Mon, 6 Jan 2014 13:37:02 -0800 Subject: [PATCH 061/890] increasing length of salt field for django apps, fixes #141 --- social/apps/django_app/default/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index 8e678c1a4..cf886b976 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -65,7 +65,7 @@ class Nonce(models.Model, DjangoNonceMixin): """One use numbers""" server_url = models.CharField(max_length=NONCE_SERVER_URL_LENGTH) timestamp = models.IntegerField() - salt = models.CharField(max_length=40) + salt = models.CharField(max_length=65) class Meta: db_table = 'social_auth_nonce' From 45e0f5a34e9ed3f1c71c498a2d83fed2969ea370 Mon Sep 17 00:00:00 2001 From: Adam Coddington Date: Mon, 6 Jan 2014 19:35:11 -0800 Subject: [PATCH 062/890] Adding Dropbox OAuth2 Support. --- social/backends/dropbox.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/social/backends/dropbox.py b/social/backends/dropbox.py index 3ac9014d7..698636334 100644 --- a/social/backends/dropbox.py +++ b/social/backends/dropbox.py @@ -2,7 +2,7 @@ Dropbox OAuth1 backend, docs at: http://psa.matiasaguirre.net/docs/backends/dropbox.html """ -from social.backends.oauth import BaseOAuth1 +from social.backends.oauth import BaseOAuth1, BaseOAuth2 class DropboxOAuth(BaseOAuth1): @@ -30,3 +30,28 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://api.dropbox.com/1/account/info', auth=self.oauth_auth(access_token)) + + +class DropboxOAuth2(BaseOAuth2): + name = 'dropbox-oauth2' + ID_KEY = 'uid' + AUTHORIZATION_URL = 'https://www.dropbox.com/1/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://api.dropbox.com/1/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('uid', 'username'), + ] + + def get_user_details(self, response): + """Return user details from Dropbox account""" + return {'username': str(response.get('uid')), + 'email': response.get('email'), + 'first_name': response.get('display_name')} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://api.dropbox.com/1/account/info', + headers={'Authorization': 'Bearer {0}'.format(access_token)} + ) From cf9e137d75c3aae5122693785011f98ce265ee4e Mon Sep 17 00:00:00 2001 From: Adam Coddington Date: Mon, 6 Jan 2014 19:42:52 -0800 Subject: [PATCH 063/890] Updating Dropbox documentation to include notes regarding OAuth2 support. --- docs/backends/dropbox.rst | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/backends/dropbox.rst b/docs/backends/dropbox.rst index 0b08689ab..a8aa62b78 100644 --- a/docs/backends/dropbox.rst +++ b/docs/backends/dropbox.rst @@ -1,13 +1,42 @@ Dropbox ======= -Dropbox uses OAuth v1.0 for authentication. +Dropbox supports both OAuth 1 and 2. -- Register a new application at `Dropbox Developers`_, and +- Register a new application at `Dropbox Developers`_, and follow the + instructions below for the version of OAuth for which you are adding + support. -- fill ``App Key`` and ``App Secret`` values in the settings:: +OAuth1 +------ + +Add the Dropbox OAuth backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.dropbox.DropboxOAuth', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: SOCIAL_AUTH_DROPBOX_KEY = '' SOCIAL_AUTH_DROPBOX_SECRET = '' +OAuth2 +------ + +Add the Dropbox OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.dropbox.DropboxOAuth2', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_DROPBOX_OAUTH2_KEY = '' + SOCIAL_AUTH_DROPBOX_OAUTH2_SECRET = '' + .. _Dropbox Developers: https://www.dropbox.com/developers/apps From f435f73a8e296d19531b3a240bffae96ab79903b Mon Sep 17 00:00:00 2001 From: Adam Coddington Date: Mon, 6 Jan 2014 19:44:51 -0800 Subject: [PATCH 064/890] Updating readme to proclaim OAuth2 support for Dropbox. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 93817f11d..3fb3e1559 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ or current ones extended): * Dailymotion_ OAuth2 * Disqus_ OAuth2 * Douban_ OAuth1 and OAuth2 - * Dropbox_ OAuth1 + * Dropbox_ OAuth1 and OAuth2 * Evernote_ OAuth1 * Exacttarget OAuth2 * Facebook_ OAuth2 and OAuth2 for Applications From 9ef69db9f1d50ef5d82605a287e6010a78f98afb Mon Sep 17 00:00:00 2001 From: Jichao Ouyang Date: Tue, 7 Jan 2014 16:18:05 +0800 Subject: [PATCH 065/890] add support for taobao --- social/tests/backends/test_taobao.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 social/tests/backends/test_taobao.py diff --git a/social/tests/backends/test_taobao.py b/social/tests/backends/test_taobao.py new file mode 100644 index 000000000..429f8345d --- /dev/null +++ b/social/tests/backends/test_taobao.py @@ -0,0 +1,37 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + +TAOBAO_OAUTH_HOST = 'oauth.taobao.com' +# TAOBAO_OAUTH_ROOT = 'authorize' +#Always use secure connection +TAOBAO_OAUTH_REQUEST_TOKEN_URL = 'https://%s/request_token' % (TAOBAO_OAUTH_HOST) +TAOBAO_OAUTH_AUTHORIZATION_URL = 'https://%s/authorize' % (TAOBAO_OAUTH_HOST) +TAOBAO_OAUTH_ACCESS_TOKEN_URL = 'https://%s/token' % (TAOBAO_OAUTH_HOST) + +TAOBAO_CHECK_AUTH = 'https://eco.taobao.com/router/rest' +TAOBAO_USER_SHOW = 'https://%s/user/get_user_info' % TAOBAO_OAUTH_HOST +class TaobaoOAuth2Test(OAuth2Test): + backend_path = 'social.backends.taobao.TAOBAOAuth' + user_data_url = TAOBAO_CHECK_AUTH + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps( { + "w2_expires_in": 0, + "taobao_user_id": "1", + "taobao_user_nick": "foobar", + "w1_expires_in": 1800, + "re_expires_in": 0, + "r2_expires_in": 0, + "expires_in": 86400, + "r1_expires_in": 1800 + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From c4306a7c66ce567c2911566555f9f3bffda931b7 Mon Sep 17 00:00:00 2001 From: Jichao Ouyang Date: Tue, 7 Jan 2014 17:41:24 +0800 Subject: [PATCH 066/890] add to django example --- .../example/templates/home.html | 4 +- social/backends/taobao.py | 46 +++++-------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index ec6b6d553..b89938445 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -61,7 +61,9 @@ Yahoo OpenId
Yahoo OAuth1
Yammer OAuth2
-Yandex OAuth2
+Yandex OAuth2 +
+TAOBAO OAuth2
Email Auth
Username Auth
diff --git a/social/backends/taobao.py b/social/backends/taobao.py index 21e2992a6..ba9e38f7f 100644 --- a/social/backends/taobao.py +++ b/social/backends/taobao.py @@ -2,65 +2,41 @@ from urllib2 import Request, urlopen, HTTPError from urllib import urlencode from urlparse import urlsplit -from social_auth.backends.exceptions import StopPipeline, AuthException, \ - AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthTokenError, \ - AuthMissingParameter -from django.conf import settings -from django.utils import simplejson import json -from django.contrib.auth import authenticate -from social_auth.backends import BaseOAuth2, OAuthBackend, USERNAME +from social.exceptions import AuthFailed +from social.backends.oauth import BaseOAuth2 # taobao OAuth base configuration TAOBAO_OAUTH_HOST = 'oauth.taobao.com' # TAOBAO_OAUTH_ROOT = 'authorize' #Always use secure connection -TAOBAO_OAUTH_REQUEST_TOKEN_URL = 'https://%s/request_token' % (TAOBAO_OAUTH_HOST) TAOBAO_OAUTH_AUTHORIZATION_URL = 'https://%s/authorize' % (TAOBAO_OAUTH_HOST) TAOBAO_OAUTH_ACCESS_TOKEN_URL = 'https://%s/token' % (TAOBAO_OAUTH_HOST) TAOBAO_CHECK_AUTH = 'https://eco.taobao.com/router/rest' -TAOBAO_USER_SHOW = 'https://%s/user/get_user_info' % TAOBAO_OAUTH_HOST -class TAOBAOBackend(OAuthBackend): - """Taobao OAuth authentication backend""" - name = 'taobao' - - def get_user_id(self, details, response): - return response['taobao_user_id'] - - def get_user_details(self, response): - """Return user details from Taobao account""" - - username = response['taobao_user_nick'] - return {USERNAME: username, - 'email': '', # not supplied - 'last_name': ''} class TAOBAOAuth(BaseOAuth2): """Taobao OAuth authentication mechanism""" + name="taobao" + ID_KEY='taobao_user_id' AUTHORIZATION_URL = TAOBAO_OAUTH_AUTHORIZATION_URL - REQUEST_TOKEN_URL = TAOBAO_OAUTH_REQUEST_TOKEN_URL ACCESS_TOKEN_URL = TAOBAO_OAUTH_ACCESS_TOKEN_URL - SERVER_URL = TAOBAO_OAUTH_HOST - AUTH_BACKEND = TAOBAOBackend - SETTINGS_KEY_NAME = 'TAOBAO_CONSUMER_KEY' - SETTINGS_SECRET_NAME = 'TAOBAO_CONSUMER_SECRET' - def user_data(self, access_token): + def user_data(self, access_token, *args, **kwargs): """Return user data provided""" params = {'method':'taobao.user.get', 'fomate':'json', 'v':'2.0', 'access_token': access_token} - - url = TAOBAO_CHECK_AUTH + urllib.urlencode(params) - # # print url - try: - return simplejson.load(urllib.urlopen(url)) + return self.get_json(TAOBAO_CHECK_AUTH, params=params) except ValueError: return None + def get_user_details(self, response): + """Return user details from Taobao account""" + username = response.get('taobao_user_nick') + return {'username': username} + # Backend definition BACKENDS = { 'taobao': TAOBAOAuth, From 0ed7107cd791dc32edbe70cdd847be44b07cb883 Mon Sep 17 00:00:00 2001 From: Jichao Ouyang Date: Tue, 7 Jan 2014 17:48:37 +0800 Subject: [PATCH 067/890] remove unused import --- social/backends/taobao.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/social/backends/taobao.py b/social/backends/taobao.py index ba9e38f7f..13032dc1a 100644 --- a/social/backends/taobao.py +++ b/social/backends/taobao.py @@ -1,7 +1,3 @@ -import urllib2,urllib -from urllib2 import Request, urlopen, HTTPError -from urllib import urlencode -from urlparse import urlsplit import json from social.exceptions import AuthFailed from social.backends.oauth import BaseOAuth2 From 182bd351505d342f7ffb10e3763ab8cf25da4429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Jan 2014 12:46:48 -0200 Subject: [PATCH 068/890] Fix dox underline --- docs/configuration/django.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index 45c90722d..0fe19664b 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -42,7 +42,7 @@ Sync database to create needed models:: Authentication backends ----------------------- +----------------------- Add desired authentication backends to Django's AUTHENTICATION_BACKENDS_ setting:: From ef28b1cb64b89a494624b94fbaff8f34680ea85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Jan 2014 13:11:42 -0200 Subject: [PATCH 069/890] Move URLs gathering to helper --- social/actions.py | 32 +++++++++++++++----------------- social/utils.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/social/actions.py b/social/actions.py index ee65b54a5..ca5694d3d 100644 --- a/social/actions.py +++ b/social/actions.py @@ -1,6 +1,6 @@ from social.p3 import quote from social.utils import sanitize_redirect, user_is_authenticated, \ - user_is_active, partial_pipeline_data + user_is_active, partial_pipeline_data, setting_url def do_auth(strategy, redirect_name='next'): @@ -34,10 +34,6 @@ def do_complete(strategy, login, user=None, redirect_name='next', is_authenticated = user_is_authenticated(user) user = is_authenticated and user or None - default_redirect = strategy.setting('LOGIN_REDIRECT_URL') - url = default_redirect - login_error_url = strategy.setting('LOGIN_ERROR_URL') or \ - strategy.setting('LOGIN_URL') partial = partial_pipeline_data(strategy, user, *args, **kwargs) if partial: @@ -52,11 +48,11 @@ def do_complete(strategy, login, user=None, redirect_name='next', if is_authenticated: if not user: - url = redirect_value or default_redirect + url = setting_url(strategy, redirect_value, 'LOGIN_REDIRECT_URL') else: - url = redirect_value or \ - strategy.setting('NEW_ASSOCIATION_REDIRECT_URL') or \ - default_redirect + url = setting_url(strategy, redirect_value, + 'NEW_ASSOCIATION_REDIRECT_URL', + 'LOGIN_REDIRECT_URL') elif user: if user_is_active(user): # catch is_new/social_user in case login() resets the instance @@ -67,22 +63,24 @@ def do_complete(strategy, login, user=None, redirect_name='next', strategy.session_set('social_auth_last_login_backend', social_user.provider) - # Remove possible redirect URL from session, if this is a new - # account, send him to the new-users-page if defined. - new_user_redirect = strategy.setting('NEW_USER_REDIRECT_URL') - if new_user_redirect and is_new: - url = new_user_redirect + if is_new: + url = setting_url(strategy, redirect_value, + 'NEW_USER_REDIRECT_URL', + 'LOGIN_REDIRECT_URL') else: - url = redirect_value or default_redirect + url = setting_url(strategy, redirect_value, + 'LOGIN_REDIRECT_URL') else: - url = strategy.setting('INACTIVE_USER_URL', login_error_url) + url = setting_url(strategy, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', + 'LOGIN_URL') else: - url = login_error_url + url = setting_url(strategy, 'LOGIN_ERROR_URL', 'LOGIN_URL') if redirect_value and redirect_value != url: redirect_value = quote(redirect_value) url += ('?' in url and '&' or '?') + \ '{0}={1}'.format(redirect_name, redirect_value) + if strategy.setting('SANITIZE_REDIRECTS', True): url = sanitize_redirect(strategy.request_host(), url) or \ strategy.setting('LOGIN_REDIRECT_URL') diff --git a/social/utils.py b/social/utils.py index 87666c400..f369dcd84 100644 --- a/social/utils.py +++ b/social/utils.py @@ -165,3 +165,20 @@ def constant_time_compare(val1, val2): for x, y in zip(val1, val2): result |= ord(x) ^ ord(y) return result == 0 + + +def is_url(value): + return value and \ + (value.startswith('http://') or + value.startswith('https://') or + value.startswith('/')) + + +def setting_url(strategy, *names): + for name in names: + if is_url(name): + return name + else: + value = strategy.setting(name) + if is_url(value): + return value From 9eb5058f19371369a7a2c06b26a28912be672462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Jan 2014 13:22:44 -0200 Subject: [PATCH 070/890] PEP8 and cleanups. Refs #145 --- social/backends/taobao.py | 40 +++++++++------------------- social/tests/backends/test_taobao.py | 30 ++++++++------------- 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/social/backends/taobao.py b/social/backends/taobao.py index 13032dc1a..c95718a46 100644 --- a/social/backends/taobao.py +++ b/social/backends/taobao.py @@ -1,39 +1,25 @@ -import json -from social.exceptions import AuthFailed from social.backends.oauth import BaseOAuth2 -# taobao OAuth base configuration -TAOBAO_OAUTH_HOST = 'oauth.taobao.com' -# TAOBAO_OAUTH_ROOT = 'authorize' -#Always use secure connection -TAOBAO_OAUTH_AUTHORIZATION_URL = 'https://%s/authorize' % (TAOBAO_OAUTH_HOST) -TAOBAO_OAUTH_ACCESS_TOKEN_URL = 'https://%s/token' % (TAOBAO_OAUTH_HOST) -TAOBAO_CHECK_AUTH = 'https://eco.taobao.com/router/rest' - + class TAOBAOAuth(BaseOAuth2): """Taobao OAuth authentication mechanism""" - name="taobao" - ID_KEY='taobao_user_id' - AUTHORIZATION_URL = TAOBAO_OAUTH_AUTHORIZATION_URL - ACCESS_TOKEN_URL = TAOBAO_OAUTH_ACCESS_TOKEN_URL - + name = 'taobao' + ID_KEY = 'taobao_user_id' + AUTHORIZATION_URL = 'https://oauth.taobao.com/authorize' + ACCESS_TOKEN_URL = 'https://oauth.taobao.com/token' + def user_data(self, access_token, *args, **kwargs): """Return user data provided""" - params = {'method':'taobao.user.get', - 'fomate':'json', - 'v':'2.0', - 'access_token': access_token} try: - return self.get_json(TAOBAO_CHECK_AUTH, params=params) + return self.get_json('https://eco.taobao.com/router/rest', params={ + 'method': 'taobao.user.get', + 'fomate': 'json', + 'v': '2.0', + 'access_token': access_token + }) except ValueError: return None def get_user_details(self, response): """Return user details from Taobao account""" - username = response.get('taobao_user_nick') - return {'username': username} - -# Backend definition -BACKENDS = { - 'taobao': TAOBAOAuth, -} + return {'username': response.get('taobao_user_nick')} diff --git a/social/tests/backends/test_taobao.py b/social/tests/backends/test_taobao.py index 429f8345d..b3d3eee1a 100644 --- a/social/tests/backends/test_taobao.py +++ b/social/tests/backends/test_taobao.py @@ -2,33 +2,25 @@ from social.tests.backends.oauth import OAuth2Test -TAOBAO_OAUTH_HOST = 'oauth.taobao.com' -# TAOBAO_OAUTH_ROOT = 'authorize' -#Always use secure connection -TAOBAO_OAUTH_REQUEST_TOKEN_URL = 'https://%s/request_token' % (TAOBAO_OAUTH_HOST) -TAOBAO_OAUTH_AUTHORIZATION_URL = 'https://%s/authorize' % (TAOBAO_OAUTH_HOST) -TAOBAO_OAUTH_ACCESS_TOKEN_URL = 'https://%s/token' % (TAOBAO_OAUTH_HOST) -TAOBAO_CHECK_AUTH = 'https://eco.taobao.com/router/rest' -TAOBAO_USER_SHOW = 'https://%s/user/get_user_info' % TAOBAO_OAUTH_HOST class TaobaoOAuth2Test(OAuth2Test): backend_path = 'social.backends.taobao.TAOBAOAuth' - user_data_url = TAOBAO_CHECK_AUTH + user_data_url = 'https://eco.taobao.com/router/rest' expected_username = 'foobar' access_token_body = json.dumps({ 'access_token': 'foobar', 'token_type': 'bearer' }) - user_data_body = json.dumps( { - "w2_expires_in": 0, - "taobao_user_id": "1", - "taobao_user_nick": "foobar", - "w1_expires_in": 1800, - "re_expires_in": 0, - "r2_expires_in": 0, - "expires_in": 86400, - "r1_expires_in": 1800 - }) + user_data_body = json.dumps({ + 'w2_expires_in': 0, + 'taobao_user_id': '1', + 'taobao_user_nick': 'foobar', + 'w1_expires_in': 1800, + 're_expires_in': 0, + 'r2_expires_in': 0, + 'expires_in': 86400, + 'r1_expires_in': 1800 + }) def test_login(self): self.do_login() From 2ee7bed329440132fe6c748bb80c23b852c46487 Mon Sep 17 00:00:00 2001 From: Jichao Ouyang Date: Wed, 8 Jan 2014 11:39:22 +0800 Subject: [PATCH 071/890] get token with POST method --- social/backends/taobao.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/taobao.py b/social/backends/taobao.py index 13032dc1a..46038712b 100644 --- a/social/backends/taobao.py +++ b/social/backends/taobao.py @@ -14,9 +14,10 @@ class TAOBAOAuth(BaseOAuth2): """Taobao OAuth authentication mechanism""" name="taobao" ID_KEY='taobao_user_id' + ACCESS_TOKEN_METHOD="POST" AUTHORIZATION_URL = TAOBAO_OAUTH_AUTHORIZATION_URL ACCESS_TOKEN_URL = TAOBAO_OAUTH_ACCESS_TOKEN_URL - + def user_data(self, access_token, *args, **kwargs): """Return user data provided""" params = {'method':'taobao.user.get', From d5bec3a8d331c78c76775cdba76f7d8361eac5e9 Mon Sep 17 00:00:00 2001 From: Jichao Ouyang Date: Wed, 8 Jan 2014 12:39:30 +0800 Subject: [PATCH 072/890] taobao docs --- README.rst | 2 ++ docs/backends/taobao.rst | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 docs/backends/taobao.rst diff --git a/README.rst b/README.rst index 3fb3e1559..37fb5550f 100644 --- a/README.rst +++ b/README.rst @@ -96,6 +96,7 @@ or current ones extended): * Steam_ OpenId * Stocktwits_ OAuth2 * Stripe_ OAuth2 + * Taobao_ OAuth2 http://open.taobao.com/doc/detail.htm?id=118 * ThisIsMyJam_ OAuth1 https://www.thisismyjam.com/developers/authentication * Trello_ OAuth1 https://trello.com/docs/gettingstarted/oauth.html * Tripit_ OAuth1 @@ -236,6 +237,7 @@ check `django-social-auth LICENSE`_ for details: .. _Soundcloud: https://soundcloud.com .. _Stocktwits: https://stocktwits.com .. _Stripe: https://stripe.com +.. _Taobao: http://open.taobao.com/doc/detail.htm?id=118 .. _Tripit: https://www.tripit.com .. _Twilio: https://www.twilio.com .. _Twitter: http://twitter.com diff --git a/docs/backends/taobao.rst b/docs/backends/taobao.rst new file mode 100644 index 000000000..b5aa5fa3f --- /dev/null +++ b/docs/backends/taobao.rst @@ -0,0 +1,16 @@ +Taobao OAuth +=========== + +Taobao OAuth 2.0 workflow. + +- Register a new application at Open Taobao_. + +- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: + + SOCIAL_AUTH_TAOBAO_KEY = '' + SOCIAL_AUTH_TAOBAO_SECRET = '' + +By default ``token``is stored in +extra_data field. + +.. _Taobao: http://open.taobao.com From 867829271406fc5bfb045312c475b0c25faa7d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 8 Jan 2014 02:57:58 -0200 Subject: [PATCH 073/890] Docs styling and PEP8 --- docs/backends/taobao.rst | 9 ++++----- social/backends/taobao.py | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/backends/taobao.rst b/docs/backends/taobao.rst index b5aa5fa3f..9606ecf12 100644 --- a/docs/backends/taobao.rst +++ b/docs/backends/taobao.rst @@ -1,16 +1,15 @@ Taobao OAuth -=========== +============ Taobao OAuth 2.0 workflow. -- Register a new application at Open Taobao_. +- Register a new application at Open `Open Taobao`_. - Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: SOCIAL_AUTH_TAOBAO_KEY = '' SOCIAL_AUTH_TAOBAO_SECRET = '' -By default ``token``is stored in -extra_data field. +By default ``token`` is stored in ``extra_data`` field. -.. _Taobao: http://open.taobao.com +.. _Open Taobao: http://open.taobao.com diff --git a/social/backends/taobao.py b/social/backends/taobao.py index 05719acf7..57839c97e 100644 --- a/social/backends/taobao.py +++ b/social/backends/taobao.py @@ -3,10 +3,9 @@ class TAOBAOAuth(BaseOAuth2): """Taobao OAuth authentication mechanism""" - name = 'taobao' ID_KEY = 'taobao_user_id' - ACCESS_TOKEN_METHOD="POST" + ACCESS_TOKEN_METHOD = 'POST' AUTHORIZATION_URL = 'https://oauth.taobao.com/authorize' ACCESS_TOKEN_URL = 'https://oauth.taobao.com/token' From 4073ac84e0764492d08d0bf088c7380520bb62e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 8 Jan 2014 02:59:01 -0200 Subject: [PATCH 074/890] Link Taobao docs on backends index --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 8f346d6af..b65a0888b 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -92,6 +92,7 @@ Social backends steam stocktwits stripe + taobao thisismyjam trello tripit From 9535a0384a342eecf74aa66b3a3e1bbb30eb1792 Mon Sep 17 00:00:00 2001 From: Roberto Robles Date: Wed, 8 Jan 2014 13:56:43 -0800 Subject: [PATCH 075/890] Fixed issue with redirect_uri with https --- social/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/strategies/base.py b/social/strategies/base.py index 4757914b0..e3fa7c277 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -186,7 +186,7 @@ def random_string(self, length=12, chars=ALLOWED_CHARS): def absolute_uri(self, path=None): uri = self.build_absolute_uri(path) - if self.setting('ON_HTTPS'): + if uri and self.setting('SOCIAL_AUTH_REDIRECT_IS_HTTPS'): uri = uri.replace('http://', 'https://') return uri From a85409ffe83c79482e80b6bd72eb2d338139b69a Mon Sep 17 00:00:00 2001 From: Roberto Robles Date: Wed, 8 Jan 2014 21:48:35 -0800 Subject: [PATCH 076/890] Remove SOCIAL_AUTH prefix on redirect_uri function --- social/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/strategies/base.py b/social/strategies/base.py index e3fa7c277..9a64c106e 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -186,7 +186,7 @@ def random_string(self, length=12, chars=ALLOWED_CHARS): def absolute_uri(self, path=None): uri = self.build_absolute_uri(path) - if uri and self.setting('SOCIAL_AUTH_REDIRECT_IS_HTTPS'): + if uri and self.setting('REDIRECT_IS_HTTPS'): uri = uri.replace('http://', 'https://') return uri From 71064a20c17589ffdf6c5eca4b08b2589552f0c9 Mon Sep 17 00:00:00 2001 From: xen Date: Fri, 10 Jan 2014 01:49:44 +0200 Subject: [PATCH 077/890] Update to follow current state in documentations --- social/apps/flask_app/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/social/apps/flask_app/models.py b/social/apps/flask_app/models.py index cca20e303..a89d3d1f3 100644 --- a/social/apps/flask_app/models.py +++ b/social/apps/flask_app/models.py @@ -19,17 +19,17 @@ class FlaskStorage(BaseSQLAlchemyStorage): code = None -def init_social(app, Base, session): +def init_social(app, db): UID_LENGTH = app.config.get(setting_name('UID_LENGTH'), 255) User = module_member(app.config[setting_name('USER_MODEL')]) - app_session = session + app_session = db.session class _AppSession(object): @classmethod def _session(cls): return app_session - class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin): + class UserSocialAuth(_AppSession, db.Model, SQLAlchemyUserMixin): """Social Auth association model""" __tablename__ = 'social_auth_usersocialauth' __table_args__ = (UniqueConstraint('provider', 'uid'),) @@ -50,7 +50,7 @@ def username_max_length(cls): def user_model(cls): return User - class Nonce(_AppSession, Base, SQLAlchemyNonceMixin): + class Nonce(_AppSession, db.Model, SQLAlchemyNonceMixin): """One use numbers""" __tablename__ = 'social_auth_nonce' __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) @@ -59,7 +59,7 @@ class Nonce(_AppSession, Base, SQLAlchemyNonceMixin): timestamp = Column(Integer) salt = Column(String(40)) - class Association(_AppSession, Base, SQLAlchemyAssociationMixin): + class Association(_AppSession, db.Model, SQLAlchemyAssociationMixin): """OpenId account association""" __tablename__ = 'social_auth_association' __table_args__ = (UniqueConstraint('server_url', 'handle'),) @@ -71,7 +71,7 @@ class Association(_AppSession, Base, SQLAlchemyAssociationMixin): lifetime = Column(Integer) assoc_type = Column(String(64)) - class Code(_AppSession, Base, SQLAlchemyCodeMixin): + class Code(_AppSession, db.Model, SQLAlchemyCodeMixin): __tablename__ = 'social_auth_code' __table_args__ = (UniqueConstraint('code', 'email'),) id = Column(Integer, primary_key=True) From 1de998b9d2111ace5176416585f0eefcf8309fc5 Mon Sep 17 00:00:00 2001 From: xen Date: Fri, 10 Jan 2014 01:51:38 +0200 Subject: [PATCH 078/890] Simplify SQLAlchemy API usage --- examples/flask_example/__init__.py | 22 ++++------------------ examples/flask_example/manage.py | 8 ++++---- examples/flask_example/models/user.py | 18 ++++++++---------- examples/flask_example/requirements.txt | 17 +++++++---------- examples/flask_example/settings.py | 4 +++- 5 files changed, 26 insertions(+), 43 deletions(-) diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py index 6b2060b43..db82c077f 100644 --- a/examples/flask_example/__init__.py +++ b/examples/flask_example/__init__.py @@ -1,15 +1,9 @@ import sys -from sqlalchemy import create_engine - from flask import Flask, g from flask.ext.sqlalchemy import SQLAlchemy from flask.ext import login -from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.ext.declarative import declarative_base - - sys.path.append('../..') from social.apps.flask_app.routes import social_auth @@ -27,21 +21,13 @@ # DB db = SQLAlchemy(app) -db.metadata.bind = create_engine(app.config['SQLALCHEMY_DATABASE_URI']) - -engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'], - convert_unicode=True) -session = scoped_session(sessionmaker(bind=engine)) -Base = declarative_base() -Base.query = session.query_property() - app.register_blueprint(social_auth) -social_storage = init_social(app, Base, session) +init_social(app, db) login_manager = login.LoginManager() login_manager.login_view = 'main' login_manager.login_message = '' -login_manager.setup_app(app) +login_manager.init_app(app) from flask_example import models from flask_example import routes @@ -63,12 +49,12 @@ def global_user(): @app.teardown_appcontext def commit_on_success(error=None): if error is None: - session.commit() + db.session.commit() @app.teardown_request def shutdown_session(exception=None): - session.remove() + db.session.remove() @app.context_processor diff --git a/examples/flask_example/manage.py b/examples/flask_example/manage.py index 36c05fabb..9953c5aa6 100755 --- a/examples/flask_example/manage.py +++ b/examples/flask_example/manage.py @@ -5,15 +5,14 @@ sys.path.append('..') -from flask_example import app, db, models, Base, engine +from flask_example import app, db manager = Manager(app) manager.add_command('runserver', Server()) manager.add_command('shell', Shell(make_context=lambda: { 'app': app, - 'db': db, - 'models': models + 'db': db })) @@ -21,7 +20,8 @@ def syncdb(): from flask_example.models import user from social.apps.flask_app import models - Base.metadata.create_all(bind=engine) + db.drop_all() + db.create_all() if __name__ == '__main__': manager.run() diff --git a/examples/flask_example/models/user.py b/examples/flask_example/models/user.py index 0ca1e920a..32727f6c1 100644 --- a/examples/flask_example/models/user.py +++ b/examples/flask_example/models/user.py @@ -1,18 +1,16 @@ -from sqlalchemy import Column, Integer, String, Boolean - from flask.ext.login import UserMixin -from flask_example import Base +from flask_example import db -class User(Base, UserMixin): +class User(db.Model, UserMixin): __tablename__ = 'users' - id = Column(Integer, primary_key=True) - username = Column(String(200)) - password = Column(String(200), default='') - name = Column(String(100)) - email = Column(String(200)) - active = Column(Boolean, default=True) + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(200)) + password = db.Column(db.String(200), default='') + name = db.Column(db.String(100)) + email = db.Column(db.String(200)) + active = db.Column(db.Boolean, default=True) def is_active(self): return self.active diff --git a/examples/flask_example/requirements.txt b/examples/flask_example/requirements.txt index 5810037aa..97de88fe2 100644 --- a/examples/flask_example/requirements.txt +++ b/examples/flask_example/requirements.txt @@ -1,10 +1,7 @@ -Flask==0.10.1 -Flask-SQLAlchemy==0.16 -Flask-Login==0.1.3 -Flask-Evolution==0.6 -Flask-Script==0.3.3 -Jinja2==2.6 -SQLAlchemy==0.7.8 -Werkzeug==0.8.3 -wsgiref==0.1.2 -pysqlite==2.6.3 +Flask +Flask-SQLAlchemy +Flask-Login +Flask-Script +Werkzeug +pysqlite +Jinja2 \ No newline at end of file diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 8fd301f93..8d3bad995 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -6,7 +6,9 @@ SECRET_KEY = 'random-secret-key' SESSION_COOKIE_NAME = 'psa_session' DEBUG = False -SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db' +from os.path import dirname, abspath +SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/test.sqlite' % dirname(abspath(__file__)) + DEBUG_TB_INTERCEPT_REDIRECTS = False SESSION_PROTECTION = 'strong' From f9321e3d66968bedbb4dcbe855d6e67f2edac400 Mon Sep 17 00:00:00 2001 From: xen Date: Fri, 10 Jan 2014 01:55:29 +0200 Subject: [PATCH 079/890] Cleanup docs --- docs/configuration/flask.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/flask.rst b/docs/configuration/flask.rst index eca098e3b..c47e92ade 100644 --- a/docs/configuration/flask.rst +++ b/docs/configuration/flask.rst @@ -33,7 +33,7 @@ and the database are defined, call ``init_social`` to register the models:: from social.apps.flask_app.models import init_social - social_storage = init_social(app, db) + init_social(app, db) So far I wasn't able to find another way to define the models on another way rather than making it as a side-effect of calling this function since the From ce8555d11e7cae48cde40006f9f2cd70a084baf3 Mon Sep 17 00:00:00 2001 From: Max Tepkeev Date: Mon, 13 Jan 2014 19:09:03 +0400 Subject: [PATCH 080/890] odnoklassniki backend iframe app fix --- docs/backends/odnoklassnikiru.rst | 8 ++++---- social/backends/odnoklassniki.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/backends/odnoklassnikiru.rst b/docs/backends/odnoklassnikiru.rst index a7d8b6cc1..8eecb7591 100644 --- a/docs/backends/odnoklassnikiru.rst +++ b/docs/backends/odnoklassnikiru.rst @@ -35,9 +35,9 @@ If you want to authenticate users in your IFrame application, - fill out some settings:: - SOCIAL_AUTH_ODNOKLASSNIKIAPP_KEY = '' - SOCIAL_AUTH_ODNOKLASSNIKIAPP_SECRET = '' - SOCIAL_AUTH_ODNOKLASSNIKIAPP_PUBLIC_NAME = '' + SOCIAL_AUTH_ODNOKLASSNIKI_APP_KEY = '' + SOCIAL_AUTH_ODNOKLASSNIKI_APP_SECRET = '' + SOCIAL_AUTH_ODNOKLASSNIKI_APP_PUBLIC_NAME = '' - add ``'social.backends.odnoklassniki.OdnoklassnikiApp'`` into your ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS`` @@ -46,7 +46,7 @@ If you want to authenticate users in your IFrame application, You may also use:: - SOCIAL_AUTH_ODNOKLASSNIKIAPP_EXTRA_USER_DATA_LIST + SOCIAL_AUTH_ODNOKLASSNIKI_APP_EXTRA_USER_DATA_LIST Defaults to empty tuple, for the list of available fields see `Documentation on user.getInfo`_ diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 13e3d0c44..9830f9bfc 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -88,8 +88,8 @@ def auth_complete(self, request, user, *args, **kwargs): return self.strategy.authenticate(*args, **kwargs) def get_auth_sig(self): - secret_key = self.setting('APP_SECRET') - hash_source = '{0:d}{1:s}{2:s}'.format(self.data['logged_user_id'], + secret_key = self.setting('SECRET') + hash_source = '{0:s}{1:s}{2:s}'.format(self.data['logged_user_id'], self.data['session_key'], secret_key) return md5(hash_source).hexdigest() From 3668ab83d925b9008bf151973a6626ffb6c583e6 Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Tue, 14 Jan 2014 17:38:40 +0100 Subject: [PATCH 081/890] moved revoking stuff to OAuthAuth class (should it be moved to BaseAuth?) --- social/backends/oauth.py | 49 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index f965d94ba..9d8e659bd 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -31,6 +31,8 @@ class OAuthAuth(BaseAuth): EXTRA_DATA = None ID_KEY = 'id' ACCESS_TOKEN_METHOD = 'GET' + REVOKE_TOKEN_URL = None + REVOKE_TOKEN_METHOD = 'POST' def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in @@ -72,6 +74,29 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service. Implement in subclass""" return {} + def revoke_token_url(self, token, uid): + return self.REVOKE_TOKEN_URL + + def revoke_token_params(self, token, uid): + return {} + + def revoke_token_headers(self, token, uid): + return {} + + def process_revoke_token_response(self, response): + return response.status_code == 200 + + def revoke_token(self, token, uid): + if self.REVOKE_TOKEN_URL: + url = self.revoke_token_url(token, uid) + params = self.revoke_token_params(token, uid) + headers = self.revoke_token_headers(token, uid) + data = urlencode(params) if self.REVOKE_TOKEN_METHOD != 'GET' \ + else None + response = self.request(url, params=params, headers=headers, + data=data, method=self.REVOKE_TOKEN_METHOD) + return self.process_revoke_token_response(response) + class BaseOAuth1(OAuthAuth): """Consumer based mechanism OAuth authentication, fill the needed @@ -214,8 +239,6 @@ class BaseOAuth2(OAuthAuth): ACCESS_TOKEN_URL = None REFRESH_TOKEN_URL = None REFRESH_TOKEN_METHOD = 'POST' - REVOKE_TOKEN_URL = None - REVOKE_TOKEN_METHOD = 'POST' RESPONSE_TYPE = 'code' REDIRECT_STATE = True STATE_PARAMETER = True @@ -367,25 +390,3 @@ def refresh_token(self, token, *args, **kwargs): request = self.request(url, **request_args) return self.process_refresh_token_response(request, *args, **kwargs) - def revoke_token_url(self, token, uid): - return self.REVOKE_TOKEN_URL - - def revoke_token_params(self, token, uid): - return {} - - def revoke_token_headers(self, token, uid): - return {} - - def process_revoke_token_response(self, response): - return response.status_code == 200 - - def revoke_token(self, token, uid): - if self.REVOKE_TOKEN_URL: - url = self.revoke_token_url(token, uid) - params = self.revoke_token_params(token, uid) - headers = self.revoke_token_headers(token, uid) - data = urlencode(params) if self.REVOKE_TOKEN_METHOD != 'GET' \ - else None - response = self.request(url, params=params, headers=headers, - data=data, method=self.REVOKE_TOKEN_METHOD) - return self.process_revoke_token_response(response) From a975764ec2cf5d1686319fa35f8b497a30fb2546 Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Tue, 14 Jan 2014 17:50:06 +0100 Subject: [PATCH 082/890] for FacebookOAuth2::process_revoke_token_response call super (solves type with 'status_code') and custom processing --- social/backends/facebook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 27c709da4..ffeb39b03 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -112,7 +112,8 @@ def revoke_token_params(self, token, uid): return {'access_token': token} def process_revoke_token_response(self, response): - return response.code == 200 and response.content == 'true' + return super(FacebookOAuth2, self).process_revoke_token_response(response) \ + and response.content == 'true' class FacebookAppOAuth2(FacebookOAuth2): From ae3b5da1120203d2339de4b954d8dcf27eff5a0d Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Tue, 14 Jan 2014 19:29:38 +0100 Subject: [PATCH 083/890] stores 'access_token' for GooglePlusAuth --- social/backends/google.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/social/backends/google.py b/social/backends/google.py index 71ef6f3d4..256bb4e04 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -79,12 +79,10 @@ class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): ('user_id', 'user_id'), ('refresh_token', 'refresh_token', True), ('expires_in', 'expires'), - ('access_type', 'access_type', True) + ('access_type', 'access_type', True), + ('code', 'code') ] - def extra_data(self, user, uid, response, details): - return {'code': response.get('code')} - def auth_complete(self, *args, **kwargs): token = self.data.get('access_token') if not token: From 7abbfed17534b8bb282751edd8f09e564a7221bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 14 Jan 2014 19:07:31 -0200 Subject: [PATCH 084/890] PEP8 --- social/backends/facebook.py | 5 +++-- social/backends/oauth.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 2edbedccf..e65a4c1d1 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -102,8 +102,9 @@ def revoke_token_params(self, token, uid): return {'access_token': token} def process_revoke_token_response(self, response): - return super(FacebookOAuth2, self).process_revoke_token_response(response) \ - and response.content == 'true' + return super(FacebookOAuth2, self).process_revoke_token_response( + response + ) and response.content == 'true' class FacebookAppOAuth2(FacebookOAuth2): diff --git a/social/backends/oauth.py b/social/backends/oauth.py index c6eede679..1772359f3 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -377,4 +377,3 @@ def refresh_token(self, token, *args, **kwargs): key: params} request = self.request(url, **request_args) return self.process_refresh_token_response(request, *args, **kwargs) - From c06612752e921c71c239337260723d9bd5590bfa Mon Sep 17 00:00:00 2001 From: harshiljain Date: Wed, 15 Jan 2014 03:51:41 -0500 Subject: [PATCH 085/890] AUTHORIZATION_URL changed to https --- social/backends/twitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index 284e8fb5d..dc3ac8627 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -9,7 +9,7 @@ class TwitterOAuth(BaseOAuth1): """Twitter OAuth authentication backend""" name = 'twitter' EXTRA_DATA = [('id', 'id')] - AUTHORIZATION_URL = 'http://api.twitter.com/oauth/authenticate' + AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authenticate' REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' From 54dd13d4f0c9bf00fad599f9dcffff371e127503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 15 Jan 2014 14:03:22 -0200 Subject: [PATCH 086/890] Raise missing parameter error in facebook. Refs #153 --- social/backends/facebook.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index e65a4c1d1..ee4804461 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -10,7 +10,8 @@ from social.utils import parse_qs, constant_time_compare from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthException, AuthCanceled, AuthUnknownError +from social.exceptions import AuthException, AuthCanceled, AuthUnknownError, \ + AuthMissingParameter class FacebookOAuth2(BaseOAuth2): @@ -51,6 +52,8 @@ def process_error(self, data): def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) + if not self.data.get('code'): + raise AuthMissingParameter(self, 'code') state = self.validate_state() key, secret = self.get_key_and_secret() url = self.ACCESS_TOKEN_URL From f0241723cacdbe7a885d058364c53e5ed03cb308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Jan 2014 12:58:59 -0200 Subject: [PATCH 087/890] v0.1.18 --- Changelog | 301 +++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 302 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index ff5b296a1..954979680 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,304 @@ +2014-01-15 v0.1.18 +================== + + * 2014-01-15 harshiljain + AUTHORIZATION_URL changed to https + + * 2014-01-14 Matías Aguirre + PEP8 + + * 2014-01-14 Javier G. Sogo + stores 'access_token' for GooglePlusAuth + + * 2014-01-14 Javier G. Sogo + for FacebookOAuth2::process_revoke_token_response call super (solves type + with 'status_code') and custom processing + + * 2014-01-14 Javier G. Sogo + moved revoking stuff to OAuthAuth class (should it be moved to BaseAuth?) + + * 2014-01-13 Max Tepkeev + odnoklassniki backend iframe app fix + + * 2014-01-10 xen + Cleanup docs + + * 2014-01-10 xen + Simplify SQLAlchemy API usage + + * 2014-01-10 xen + Update to follow current state in documentations + + * 2014-01-08 Roberto Robles + Remove SOCIAL_AUTH prefix on redirect_uri function + + * 2014-01-08 Roberto Robles + Fixed issue with redirect_uri with https + + * 2014-01-08 Matías Aguirre + Link Taobao docs on backends index + + * 2014-01-08 Matías Aguirre + Docs styling and PEP8 + + * 2014-01-08 Jichao Ouyang + taobao docs + + * 2014-01-08 Jichao Ouyang + get token with POST method + + * 2014-01-07 Matías Aguirre + PEP8 and cleanups. Refs #145 + + * 2014-01-07 Matías Aguirre + Move URLs gathering to helper + + * 2014-01-07 Matías Aguirre + Fix dox underline + + * 2014-01-07 Jichao Ouyang + remove unused import + + * 2014-01-07 Jichao Ouyang + add to django example + + * 2014-01-07 Jichao Ouyang + add support for taobao + + * 2014-01-06 Adam Coddington + Updating readme to proclaim OAuth2 support for Dropbox. + + * 2014-01-06 Adam Coddington + Updating Dropbox documentation to include notes regarding OAuth2 support. + + * 2014-01-06 Adam Coddington + Adding Dropbox OAuth2 Support. + + * 2014-01-06 Edwin Knuth + increasing length of salt field for django apps, fixes #141 + + * 2014-01-06 Matías Aguirre + Simplify partial handling on actions + + * 2014-01-06 Matías Aguirre + Updated readme with other dependencies. Closes #140 + + * 2014-01-06 Jichao Ouyang + add support for taobao + + * 2014-01-04 Matías Aguirre + Always send email validations is required + + * 2014-01-04 Matías Aguirre + Move extra-data logic to base clase + + * 2014-01-02 Matías Aguirre + Fix ID_KEY for Tumblr backend. Refs #136 + + * 2014-01-02 Matías Aguirre + Use cases doc. Refs #137 + + * 2014-01-01 Matías Aguirre + Fix docstring. Refs #136 + + * 2013-12-28 Matías Aguirre + Line chars limit in docs. Refs #135 + + * 2013-12-28 Matías Aguirre + PEP8. Refs #135 + + * 2013-12-28 Xmypblu + Add support for OpenStreetMap OAuth + + * 2013-12-27 Matías Aguirre + Update porting docs regarding session value + + * 2013-12-27 Matías Aguirre + PEP8 + + * 2013-12-26 Nicolas Cortot + Support for MongoEngine authentication using Custom User Model + + * 2013-12-25 Nick Sullivan + Update reddit.py + + * 2013-12-17 Jay Parlar + Tiny typo fix + + * 2013-12-16 maxtepkeev + fix session expiration in vk backend + + * 2013-12-11 Kevin Tran + Added support for named URLs and URL translation using the django built-in + resolve_url before giving the url to tje HttpResponseRedirect. See also + https://code.djangoproject.com/ticket/15552 + + * 2013-12-11 Bob Alcorn + Updated pipeline example to include externalized auth; + + * 2013-12-09 Matías Aguirre + Avoid broken email entries on yahoo API. Closes #125 + + * 2013-12-09 Matías Aguirre + Allow unauthorized token retrieval/storage overrideable. Refs #111 + + * 2013-12-07 Matías Aguirre + Constant type compare on HMAC signatures. Closes #122 + + * 2013-12-07 monkut + Removed non-ascii character from author string + + * 2013-12-06 Hans + Add test backends to the package. + + * 2013-12-06 Rodrigue Villetard + Missing trailing slash on complete url + + * 2013-12-03 Matías Aguirre + PEP8. Refs #116 + + * 2013-12-03 Stephen McDonald + Add refs to getpocket.com in readme + docs + + * 2013-12-03 Stephen McDonald + getpocket.com backend + + * 2013-12-02 Matías Aguirre + Helper to get current backend instance. Refs #114 + + * 2013-11-30 Matías Aguirre + Set current strategy on pyramid app + + * 2013-11-30 Matías Aguirre + Simplify pyramid settings access + + * 2013-11-30 Matías Aguirre + Set current strategy on webpy and flask apps + + * 2013-11-29 Matías Aguirre + PEP8 + + * 2013-11-28 Matías Aguirre + Link to backends docs in the modules instead of repeating the docs. Refs + #107 + + * 2013-11-28 Matías Aguirre + Yammer docs + + * 2013-11-28 Matías Aguirre + Improves to Yahoo docs + + * 2013-11-28 Matías Aguirre + Xing docs + + * 2013-11-28 Matías Aguirre + Trello docs + + * 2013-11-28 Matías Aguirre + Podio docs + + * 2013-11-28 Matías Aguirre + Mendeley docs + + * 2013-11-28 Matías Aguirre + Fix backends index order + + * 2013-11-28 Matías Aguirre + LiveJournal docs + + * 2013-11-28 Matías Aguirre + Jawbone docs + + * 2013-11-28 Matías Aguirre + Foursquare backend docs + + * 2013-11-28 Matías Aguirre + Fitbit docs + + * 2013-11-28 Matías Aguirre + Fedora openid docs + + * 2013-11-28 Matías Aguirre + Fix douban oauth1 title + + * 2013-11-28 Matías Aguirre + Dailymotion docs + + * 2013-11-28 Matías Aguirre + File format fix to coinbase docs + + * 2013-11-28 Matías Aguirre + Fix backends order + + * 2013-11-28 Matías Aguirre + BelgiumEID docs + + * 2013-11-28 Matías Aguirre + AOL docs + + * 2013-11-26 Norton Wang + fix uid in coinbase oauth + + * 2013-11-23 Norton Wang + add coinbase docs, add runkeeper docs to index + + * 2013-11-23 Norton Wang + add coinbase oauth + + * 2013-11-23 Norton Wang + Add more examples to django_example, alphabetize, fix some grammar + + * 2013-11-21 Matías Aguirre + Fix setting name in docs. Refs #97 + + * 2013-11-21 Matías Aguirre + Move default pipeline definitions to constants for easy import. Refs #99 + + * 2013-11-21 josseph + Update weibo.py + + * 2013-11-20 maxtepkeev + Make vk-app backend to retrieve additional user data in respect to the + *_EXTRA_DATA setting + + * 2013-11-19 Matías Aguirre + Fix typo + + * 2013-11-19 Matías Aguirre + Mention callback URL definition on linkedin when using oauth2. Refs #58 + + * 2013-11-18 Matías Aguirre + Include backend name in setting if backend is defined. Refs #95 + + * 2013-11-18 Matías Aguirre + PEP8 and simplifications. Refs #92 + + * 2013-11-18 Matías Aguirre + Restore prvious link, fix schema in readthedocs link. Refs #93 + + * 2013-11-17 Sahil Gupta + Updated README to point to the latest docs on Read The Docs. + + * 2013-11-16 Marios + Google Plus backend allows for a server-side flow that can grant a refresh + token that can be subsequently used to perform operations on behalf of the + user, even if the user is not online. + + * 2013-11-14 Matías Aguirre + Replace format call with string join. Closes #91 + + * 2013-11-14 Juan Riaza + a better way + + * 2013-11-14 Juan Riaza + fitbit uid + + * 2013-11-13 Matías Aguirre + Fix OpenId PAPE max age check. Closes #89 + + * 2013-11-13 Matías Aguirre + Changelog update + 2013-11-13 v0.1.17 ================== diff --git a/social/__init__.py b/social/__init__.py index bbaad4828..949416aec 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 17) +version = (0, 1, 18) extra = '' __version__ = '.'.join(map(str, version)) + extra From 22b251ca417702f1eacff9061d6c6008106dd422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Jan 2014 13:27:36 -0200 Subject: [PATCH 088/890] Generate packages names dynamically --- setup.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 228b1b1e6..93793e5e8 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """Setup file for easy installation""" import sys -from os.path import join, dirname +import os +from os.path import join, dirname, split from setuptools import setup @@ -28,6 +29,23 @@ def long_description(): return LONG_DESCRIPTION +def path_tokens(path): + if not path: + return [] + head, tail = os.path.split(path) + return path_tokens(head) + [tail] + + +def get_packages(): + exclude_pacakages = ('__pycache__',) + packages = [] + for path_info in os.walk('social'): + tokens = path_tokens(path_info[0]) + if tokens[-1] not in exclude_pacakages: + packages.append('.'.join(tokens)) + return packages + + requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0'] if PY3: requires += ['python3-openid>=3.0.1', @@ -35,6 +53,7 @@ def long_description(): else: requires += ['python-openid>=2.2', 'requests-oauthlib>=0.3.0'] + setup(name='python-social-auth', version=version, author='Matias Aguirre', @@ -43,20 +62,7 @@ def long_description(): license='BSD', keywords='django, flask, pyramid, webpy, openid, oauth, social auth', url='https://github.com/omab/python-social-auth', - packages=['social', - 'social.storage', - 'social.apps', - 'social.apps.django_app', - 'social.apps.django_app.default', - 'social.apps.django_app.me', - 'social.apps.webpy_app', - 'social.apps.flask_app', - 'social.backends', - 'social.pipeline', - 'social.strategies', - 'social.tests.actions', - 'social.tests.backends', - 'social.tests'], + packages=get_packages(), #package_data={'social': ['locale/*/LC_MESSAGES/*']}, long_description=long_description(), install_requires=requires, From 4a1034fce36ca400bc30bbf95504778235714417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Jan 2014 13:29:11 -0200 Subject: [PATCH 089/890] v0.1.19 --- Changelog | 12 +++++++++++- social/__init__.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 954979680..8cbed3540 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,16 @@ -2014-01-15 v0.1.18 +2014-01-16 v0.1.19 ================== + * 2014-01-16 Matías Aguirre + Generate packages names dynamically + + +2014-01-16 v0.1.18 +================== + + * 2014-01-15 Matías Aguirre + Raise missing parameter error in facebook. Refs #153 + * 2014-01-15 harshiljain AUTHORIZATION_URL changed to https diff --git a/social/__init__.py b/social/__init__.py index 949416aec..059b0043e 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 18) +version = (0, 1, 19) extra = '' __version__ = '.'.join(map(str, version)) + extra From 91eaf6c4d540f50374fff4b1cff299eeb04c5327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Jan 2014 13:46:22 -0200 Subject: [PATCH 090/890] Also support old keys format in linkedin backend for basic data --- social/backends/linkedin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index 7ab99381e..4821d8c9e 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -7,8 +7,10 @@ class BaseLinkedinAuth(object): EXTRA_DATA = [('id', 'id'), - ('first-name', 'first_name'), - ('last-name', 'last_name')] + ('first-name', 'first_name', True), + ('last-name', 'last_name', True), + ('firstName', 'first_name', True), + ('lastName', 'last_name', True)] USER_DETAILS = 'https://api.linkedin.com/v1/people/~:({0})' def get_user_details(self, response): From 9bd1bfc908435280d8a8e7f28a8f3b2a723bcf55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Jan 2014 20:55:14 -0200 Subject: [PATCH 091/890] Fix linkedin docs about attributes names. Closes #161 --- docs/backends/linkedin.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backends/linkedin.rst b/docs/backends/linkedin.rst index c552b1945..a7f9ccc87 100644 --- a/docs/backends/linkedin.rst +++ b/docs/backends/linkedin.rst @@ -45,9 +45,9 @@ would add these settings:: SOCIAL_AUTH_LINKEDIN_FIELD_SELECTORS = ['email-address', 'headline', 'industry'] # Arrange to add the fields to UserSocialAuth.extra_data SOCIAL_AUTH_LINKEDIN_EXTRA_DATA = [('id', 'id'), - ('first-name', 'first_name'), - ('last-name', 'last_name'), - ('email-address', 'email_address'), + ('firstName', 'first_name'), + ('lastName', 'last_name'), + ('emailAddress', 'email_address'), ('headline', 'headline'), ('industry', 'industry')] From 6c4d5dbb2a0e76eb08070f25cb27b909014894bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 17 Jan 2014 11:39:24 -0200 Subject: [PATCH 092/890] Decode bytes on Python3 otherwise it breaks session saving on Django. Refs #139 --- social/backends/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 1772359f3..19e1ff873 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -171,7 +171,7 @@ def unauthorized_token(self): callback_uri=self.redirect_uri, decoding=decoding), method=self.REQUEST_TOKEN_METHOD) - return response.content + return response.content.decode(response.encoding) def oauth_authorization_request(self, token): """Generate OAuth request to authorize token.""" From 461f4f91ad1820b7c7d354173979d00352e91ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 17 Jan 2014 11:55:10 -0200 Subject: [PATCH 093/890] v0.1.20 --- Changelog | 71 +++++++++++++++++++++++++++++++++++++++++++++- social/__init__.py | 2 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 8cbed3540..58d42c98f 100644 --- a/Changelog +++ b/Changelog @@ -1,13 +1,31 @@ +2014-01-17 v0.1.20 +================== + + * 2014-01-17 Matías Aguirre + Decode bytes on Python3 otherwise it breaks session saving on Django. Refs + #139 + + * 2014-01-16 Matías Aguirre + Fix linkedin docs about attributes names. Closes #161 + + * 2014-01-16 Matías Aguirre + Also support old keys format in linkedin backend for basic data + 2014-01-16 v0.1.19 ================== * 2014-01-16 Matías Aguirre - Generate packages names dynamically + v0.1.19 + * 2014-01-16 Matías Aguirre + Generate packages names dynamically 2014-01-16 v0.1.18 ================== + * 2014-01-16 Matías Aguirre + v0.1.18 + * 2014-01-15 Matías Aguirre Raise missing parameter error in facebook. Refs #153 @@ -312,6 +330,9 @@ 2013-11-13 v0.1.17 ================== + * 2013-11-13 Matías Aguirre + v0.1.17 + * 2013-11-13 Matías Aguirre Support remember flag when calling login on flask app @@ -361,6 +382,9 @@ 2013-11-07 v0.1.16 ================== + * 2013-11-07 Matías Aguirre + v0.1.16 + * 2013-11-07 Matías Aguirre Remove unused vars @@ -410,6 +434,9 @@ 2013-11-04 v0.1.15 ================== + * 2013-11-04 Matías Aguirre + v0.1.15 + * 2013-11-04 Matías Aguirre Test runkeeper backend @@ -516,6 +543,9 @@ 2013-10-07 v0.1.14 ================== + * 2013-10-07 Matías Aguirre + v0.1.14 + * 2013-10-07 Matías Aguirre Fix encoding string between python2 and 3 @@ -656,6 +686,9 @@ 2013-09-22 v0.1.13 ================== + * 2013-09-22 Matías Aguirre + v0.1.13 + * 2013-09-22 Matías Aguirre Move common code to base class @@ -795,6 +828,9 @@ 2013-09-13 v0.1.12 ================== + * 2013-09-13 Matías Aguirre + v0.1.12 + * 2013-09-12 Matías Aguirre Fix get_social_auth_for_user on mongoengine storage @@ -879,6 +915,9 @@ 2013-09-03 v0.1.11 ================== + * 2013-09-03 Matías Aguirre + v0.1.11 + * 2013-09-03 Matías Aguirre Enforce list on pipeline method @@ -975,6 +1014,9 @@ 2013-08-29 v0.1.10 ================== + * 2013-08-29 Matías Aguirre + v0.1.10 + * 2013-08-29 Matías Aguirre PEP8 @@ -987,6 +1029,9 @@ 2013-08-29 v0.1.9 ================= + * 2013-08-29 Matías Aguirre + v0.1.9 + * 2013-08-29 Matías Aguirre Allow to override strategy getter @@ -1056,6 +1101,9 @@ 2013-07-13 v0.1.8 ================= + * 2013-07-13 Matías Aguirre + v0.1.8 + * 2013-07-13 Matías Aguirre Add method to determine if current user is allowed to login @@ -1089,6 +1137,9 @@ 2013-06-03 v0.1.7 ================= + * 2013-06-03 Matías Aguirre + v0.1.7 + * 2013-06-03 Matías Aguirre Fix inheritance on flask and sqlalchemy orm @@ -1098,12 +1149,18 @@ 2013-06-03 v0.1.6 ================= + * 2013-06-03 Matías Aguirre + v0.1.6 + * 2013-06-03 Matías Aguirre Enforce db session passing on flask init 2013-06-01 v0.1.5 ================= + * 2013-06-01 Matías Aguirre + v0.1.5 + * 2013-06-01 Matías Aguirre Simpler code to convert values to and from session @@ -1125,6 +1182,9 @@ 2013-05-31 v0.1.4 ================= + * 2013-05-31 Matías Aguirre + v0.1.4 + * 2013-05-31 Matías Aguirre Unrestricted user fields on instance creation, defaults to username and email @@ -1132,6 +1192,9 @@ 2013-05-31 v0.1.3 ================= + * 2013-05-31 Matías Aguirre + v0.1.3 + * 2013-05-30 Matías Aguirre Avoid version 0.3.2 of requests-oauthlib on python 3 (setup.py) @@ -1226,6 +1289,9 @@ 2013-04-03 v0.1.2 ================= + * 2013-04-03 Matías Aguirre + v0.1.2 + * 2013-04-03 Matías Aguirre Update tests docs @@ -1294,6 +1360,9 @@ 2013-04-01 v0.1.1 ================= + * 2013-04-01 Matías Aguirre + v0.1.1 + * 2013-04-01 Matías Aguirre Use a default dict to play with the console and django strategy diff --git a/social/__init__.py b/social/__init__.py index 059b0043e..6160bd18b 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 19) +version = (0, 1, 20) extra = '' __version__ = '.'.join(map(str, version)) + extra From 9d45d50c4cd064a33f8303592eea41d90750c296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 17 Jan 2014 16:03:40 -0200 Subject: [PATCH 094/890] Override get_user_id on tumblr backend. Refs #136 --- social/backends/tumblr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/backends/tumblr.py b/social/backends/tumblr.py index 9bfd8261d..3d1eda2c7 100644 --- a/social/backends/tumblr.py +++ b/social/backends/tumblr.py @@ -14,6 +14,9 @@ class TumblrOAuth(BaseOAuth1): REQUEST_TOKEN_METHOD = 'POST' ACCESS_TOKEN_URL = 'http://www.tumblr.com/oauth/access_token' + def get_user_id(self, details, response): + return response['response']['user'][self.ID_KEY] + def get_user_details(self, response): # http://www.tumblr.com/docs/en/api/v2#user-methods user_info = response['response']['user'] From 9f27a678914c474afec3fd30cd5ae4ced2408d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 18 Jan 2014 12:30:42 -0200 Subject: [PATCH 095/890] Snippet to get people from circles on Google+ --- docs/use_cases.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 28faf59d0..ddea4de59 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -19,4 +19,29 @@ Django:: Login with Facebook +Retrieve Google+ Friends +------------------------ + +Google provides a `People API endpoint`_ to retrieve the people in your circles +on Google+. In order to access that API first we need to define the needed +scope:: + + SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ + 'https://www.googleapis.com/auth/plus.login' + ] + +Once we have the ``access token`` we can call the API like this:: + + import requests + + user = User.objects.get(...) + social = user.social_auth.get(provider='google-oauth2') + response = requests.get( + 'https://www.googleapis.com/plus/v1/people/me/people/visible', + params={'access_token': social.extra_data['access_token']} + ) + friends = response.json()['items'] + + .. _python-social-auth: https://github.com/omab/python-social-auth +.. _People API endpoint: https://developers.google.com/+/api/latest/people/list From 61fae8775fc67db8a8ab7d019504b24e02cd0339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 18 Jan 2014 13:22:08 -0200 Subject: [PATCH 096/890] Support Weibo domain as username by setting. Closes #164 --- docs/backends/weibo.rst | 7 +++++++ social/backends/weibo.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/backends/weibo.rst b/docs/backends/weibo.rst index 341e66752..4d933868c 100644 --- a/docs/backends/weibo.rst +++ b/docs/backends/weibo.rst @@ -13,4 +13,11 @@ Weibo OAuth 2.0 workflow. By default ``account id``, ``profile_image_url`` and ``gender`` are stored in extra_data field. +The user name is used by default to build the user instance ``username``, +sometimes this contains non-ASCII characters which might not be desirable for +the website. To avoid this issue it's possible to use the Weibo ``domain`` +which will be inside the ASCII range by defining this setting:: + + SOCIAL_AUTH_WEIBO_DOMAIN_AS_USERNAME = True + .. _Weibo: http://open.weibo.com diff --git a/social/backends/weibo.py b/social/backends/weibo.py index c9cec8c47..1e287c9ef 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -27,7 +27,11 @@ def get_user_details(self, response): """Return user details from Weibo. API URL is: https://api.weibo.com/2/users/show.json/?uid=&access_token= """ - return {'username': response.get("name", ""), + if self.setting('DOMAIN_AS_USERNAME'): + username = response.get('domain', '') + else: + username = response.get('name', '') + return {'username': username, 'first_name': response.get('screen_name', '')} def user_data(self, access_token, *args, **kwargs): From 96b05710d1c8964012ddf47fed0b1c5c6b5c98e0 Mon Sep 17 00:00:00 2001 From: Yasin Aktimur Date: Mon, 20 Jan 2014 02:29:10 +0200 Subject: [PATCH 097/890] Serializer changed. --- examples/django_example/example/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index a5f49b513..d577beb26 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -107,7 +107,7 @@ } } -SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer' +SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' TEMPLATE_CONTEXT_PROCESSORS = ( 'django.contrib.auth.context_processors.auth', From 3493cfa84aedaafcf79be9324a87c5dca77f168c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 20 Jan 2014 03:03:11 -0200 Subject: [PATCH 098/890] Use same DB name as other examples --- examples/flask_example/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 8d3bad995..9ea3d7a13 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -7,7 +7,7 @@ SESSION_COOKIE_NAME = 'psa_session' DEBUG = False from os.path import dirname, abspath -SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/test.sqlite' % dirname(abspath(__file__)) +SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/test.db' % dirname(abspath(__file__)) DEBUG_TB_INTERCEPT_REDIRECTS = False SESSION_PROTECTION = 'strong' From 7fb97c58868b5058242b1d7b11eef3fa34bf6ca3 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 21 Jan 2014 12:00:30 +0000 Subject: [PATCH 099/890] Added new PixelPin provider. --- social/backends/pixelpin.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 social/backends/pixelpin.py diff --git a/social/backends/pixelpin.py b/social/backends/pixelpin.py new file mode 100644 index 000000000..4e50a0608 --- /dev/null +++ b/social/backends/pixelpin.py @@ -0,0 +1,28 @@ +from social.backends.oauth import BaseOAuth2 +import urllib +import json + +class PixelPinOAuth2(BaseOAuth2): + """PixelPin OAuth authentication backend""" + name = 'pixelpin-oauth2' + AUTHORIZATION_URL = 'https://login.pixelpin.co.uk/OAuth2/Flogin.aspx' + ACCESS_TOKEN_URL = 'https://ws3.pixelpin.co.uk/index.php/api/token' + ACCESS_TOKEN_METHOD = 'POST' + ID_KEY = 'id' + REQUIRES_EMAIL_VALIDATION = False + EXTRA_DATA = [ + ('id', 'id'), + ] + + def get_user_details(self, response): + """Return user details from PixelPin account""" + return {'username': response.get('firstName'), + 'email': response.get('email') or '', + 'first_name': response.get('firstName')} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://ws3.pixelpin.co.uk/index.php/api/userdata', + params={'access_token': access_token} + ) \ No newline at end of file From 7df0caacede452c936b8608f9f955351e103dcaa Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 21 Jan 2014 15:12:44 +0000 Subject: [PATCH 100/890] Add documentation for PixelPin --- docs/backends/pixelpin.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/backends/pixelpin.rst diff --git a/docs/backends/pixelpin.rst b/docs/backends/pixelpin.rst new file mode 100644 index 000000000..b623dcb4e --- /dev/null +++ b/docs/backends/pixelpin.rst @@ -0,0 +1,30 @@ +PixelPin +======== + +PixelPin itself supports OAuth 1 and 2 but this provider only supports OAuth2. + +PixelPin OAuth2 +--------------- + +Developer documentation for PixelPin can be found at https://login.pixelpin.co.uk/Developer/Index.aspx +To setup OAuth2 do the following: + +- Register a new developer account at `PixelPin Developers`_. + + You require a PixelPin account to create developer accounts. Sign up at `PixelPin Account Page`_ + For the value of redirect uri, use whatever path you need to return to on your web application. The + example code provided with the plugin uses http:///complete/pixelpin-oauth2/ + + Once verified by email, record the values of client id and secret for the next step + +- Fill **Consumer Key** and **Consumer Secret** values in your settings.py file:: + + SOCIAL_AUTH_PIXELPIN_OAUTH2_KEY = '' + SOCIAL_AUTH_PIXELPIN_OAUTH2_SECRET = '' + +- Add ``'social.backends.pixelpin.PixelPinOAuth2'`` into your + ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. + +.. _PixelPin homepage: http://pixelpin.co.uk/ +.. _PixelPin Account Page: https://login.pixelpin.co.uk/ +.. _PixelPin Developers: https://login.pixelpin.co.uk/Developers/Index.aspx From 447c902df234444bf5d057d0ef8949838452ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 21 Jan 2014 13:19:52 -0200 Subject: [PATCH 101/890] PEP8, file formats and line lengths fixes --- docs/backends/pixelpin.rst | 64 ++++++++++++++++++++----------------- social/backends/pixelpin.py | 55 ++++++++++++++++--------------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/docs/backends/pixelpin.rst b/docs/backends/pixelpin.rst index b623dcb4e..8479a9349 100644 --- a/docs/backends/pixelpin.rst +++ b/docs/backends/pixelpin.rst @@ -1,30 +1,34 @@ -PixelPin -======== - -PixelPin itself supports OAuth 1 and 2 but this provider only supports OAuth2. - -PixelPin OAuth2 ---------------- - -Developer documentation for PixelPin can be found at https://login.pixelpin.co.uk/Developer/Index.aspx -To setup OAuth2 do the following: - -- Register a new developer account at `PixelPin Developers`_. - - You require a PixelPin account to create developer accounts. Sign up at `PixelPin Account Page`_ - For the value of redirect uri, use whatever path you need to return to on your web application. The - example code provided with the plugin uses http:///complete/pixelpin-oauth2/ - - Once verified by email, record the values of client id and secret for the next step - -- Fill **Consumer Key** and **Consumer Secret** values in your settings.py file:: - - SOCIAL_AUTH_PIXELPIN_OAUTH2_KEY = '' - SOCIAL_AUTH_PIXELPIN_OAUTH2_SECRET = '' - -- Add ``'social.backends.pixelpin.PixelPinOAuth2'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - -.. _PixelPin homepage: http://pixelpin.co.uk/ -.. _PixelPin Account Page: https://login.pixelpin.co.uk/ -.. _PixelPin Developers: https://login.pixelpin.co.uk/Developers/Index.aspx +PixelPin +======== + +PixelPin itself supports OAuth 1 and 2 but this provider only supports OAuth2. + +PixelPin OAuth2 +--------------- + +Developer documentation for PixelPin can be found at +https://login.pixelpin.co.uk/Developer/Index.aspx To setup OAuth2 do the +following: + +- Register a new developer account at `PixelPin Developers`_. + + You require a PixelPin account to create developer accounts. Sign up at + `PixelPin Account Page`_ For the value of redirect uri, use whatever path you + need to return to on your web application. The example code provided with the + plugin uses ``http:///complete/pixelpin-oauth2/``. + + Once verified by email, record the values of client id and secret for the + next step. + +- Fill **Consumer Key** and **Consumer Secret** values in your settings.py + file:: + + SOCIAL_AUTH_PIXELPIN_OAUTH2_KEY = '' + SOCIAL_AUTH_PIXELPIN_OAUTH2_SECRET = '' + +- Add ``'social.backends.pixelpin.PixelPinOAuth2'`` into your + ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. + +.. _PixelPin homepage: http://pixelpin.co.uk/ +.. _PixelPin Account Page: https://login.pixelpin.co.uk/ +.. _PixelPin Developers: https://login.pixelpin.co.uk/Developers/Index.aspx diff --git a/social/backends/pixelpin.py b/social/backends/pixelpin.py index 4e50a0608..b2e224145 100644 --- a/social/backends/pixelpin.py +++ b/social/backends/pixelpin.py @@ -1,28 +1,27 @@ -from social.backends.oauth import BaseOAuth2 -import urllib -import json - -class PixelPinOAuth2(BaseOAuth2): - """PixelPin OAuth authentication backend""" - name = 'pixelpin-oauth2' - AUTHORIZATION_URL = 'https://login.pixelpin.co.uk/OAuth2/Flogin.aspx' - ACCESS_TOKEN_URL = 'https://ws3.pixelpin.co.uk/index.php/api/token' - ACCESS_TOKEN_METHOD = 'POST' - ID_KEY = 'id' - REQUIRES_EMAIL_VALIDATION = False - EXTRA_DATA = [ - ('id', 'id'), - ] - - def get_user_details(self, response): - """Return user details from PixelPin account""" - return {'username': response.get('firstName'), - 'email': response.get('email') or '', - 'first_name': response.get('firstName')} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://ws3.pixelpin.co.uk/index.php/api/userdata', - params={'access_token': access_token} - ) \ No newline at end of file +from social.backends.oauth import BaseOAuth2 + + +class PixelPinOAuth2(BaseOAuth2): + """PixelPin OAuth authentication backend""" + name = 'pixelpin-oauth2' + ID_KEY = 'id' + AUTHORIZATION_URL = 'https://login.pixelpin.co.uk/OAuth2/Flogin.aspx' + ACCESS_TOKEN_URL = 'https://ws3.pixelpin.co.uk/index.php/api/token' + ACCESS_TOKEN_METHOD = 'POST' + REQUIRES_EMAIL_VALIDATION = False + EXTRA_DATA = [ + ('id', 'id'), + ] + + def get_user_details(self, response): + """Return user details from PixelPin account""" + return {'username': response.get('firstName'), + 'email': response.get('email') or '', + 'first_name': response.get('firstName')} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://ws3.pixelpin.co.uk/index.php/api/userdata', + params={'access_token': access_token} + ) From 101741959973c6d9d3122e52788cd017a4c48650 Mon Sep 17 00:00:00 2001 From: lukos Date: Tue, 21 Jan 2014 15:40:50 +0000 Subject: [PATCH 102/890] Added PixelPin to list of providers --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 37fb5550f..296a07012 100644 --- a/README.rst +++ b/README.rst @@ -84,6 +84,7 @@ or current ones extended): * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth * OpenSuse_ OpenId http://en.opensuse.org/openSUSE:Connect * Orkut_ OAuth1 + * PixelPin_ OAuth2 * Pocket_ OAuth2 * Podio_ OAuth2 * Rdio_ OAuth1 and OAuth2 @@ -274,3 +275,4 @@ check `django-social-auth LICENSE`_ for details: .. _OpenStreetMap: http://www.openstreetmap.org .. _six: http://pythonhosted.org/six/ .. _requests: http://docs.python-requests.org/en/latest/ +.. _PixelPin: http://pixelpin.co.uk From df6e0ea744b930d01c3dd80ab2f6e6e619c6b325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 21 Jan 2014 13:44:05 -0200 Subject: [PATCH 103/890] Ensure encode() before md5 call for python3. Closes #168 --- social/backends/mailru.py | 4 +++- social/backends/odnoklassniki.py | 15 +++++++++------ social/backends/vk.py | 8 +++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/social/backends/mailru.py b/social/backends/mailru.py index 7d6eaa859..acce475af 100644 --- a/social/backends/mailru.py +++ b/social/backends/mailru.py @@ -37,6 +37,8 @@ def user_data(self, access_token, *args, **kwargs): 'app_id': key, 'secure': '1'} param_list = sorted(list(item + '=' + data[item] for item in data)) - data['sig'] = md5(''.join(param_list) + secret).hexdigest() + data['sig'] = md5( + (''.join(param_list) + secret).encode('utf-8') + ).hexdigest() return self.get_json('http://www.appsmail.ru/platform/api', params=data)[0] diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 9830f9bfc..866a40ff0 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -92,7 +92,7 @@ def get_auth_sig(self): hash_source = '{0:s}{1:s}{2:s}'.format(self.data['logged_user_id'], self.data['session_key'], secret_key) - return md5(hash_source).hexdigest() + return md5(hash_source.encode('utf-8')).hexdigest() def get_response(self): fields = ('logged_user_id', 'api_server', 'application_key', @@ -115,12 +115,14 @@ def odnoklassniki_oauth_sig(data, client_secret): http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=12878032, search for "little bit different way" """ - suffix = md5('{0:s}{1:s}'.format(data['access_token'], - client_secret)).hexdigest() + suffix = md5( + '{0:s}{1:s}'.format(data['access_token'], + client_secret).encode('utf-8') + ).hexdigest() check_list = sorted(['{0:s}={1:s}'.format(key, value) for key, value in data.items() if key != 'access_token']) - return md5(''.join(check_list) + suffix).hexdigest() + return md5((''.join(check_list) + suffix).encode('utf-8')).hexdigest() def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): @@ -133,8 +135,9 @@ def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): """ param_list = sorted(['{0:s}={1:s}'.format(key, value) for key, value in data.items()]) - return md5(''.join(param_list) + - client_secret_or_session_secret).hexdigest() + return md5( + (''.join(param_list) + client_secret_or_session_secret).encode('utf-8') + ).hexdigest() def odnoklassniki_api(backend, data, api_url, public_key, client_secret, diff --git a/social/backends/vk.py b/social/backends/vk.py index c244bd9f1..e676f36ba 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -55,7 +55,7 @@ def auth_complete(self, *args, **kwargs): check_str = ''.join(item + '=' + cookie_dict[item] for item in ['expire', 'mid', 'secret', 'sid']) - hash = md5(check_str + secret).hexdigest() + hash = md5((check_str + secret).encode('utf-8')).hexdigest() if hash != cookie_dict['sig'] or int(cookie_dict['expire']) < time(): raise ValueError('VK.com authentication failed: invalid hash') @@ -144,7 +144,7 @@ def auth_complete(self, *args, **kwargs): if auth_key: check_key = md5('_'.join([key, self.data.get('viewer_id'), - secret])).hexdigest() + secret]).encode('utf-8')).hexdigest() if check_key != auth_key: raise ValueError('VK.com authentication failed: invalid ' 'auth key') @@ -192,7 +192,9 @@ def vk_api(backend, method, data): data['format'] = 'json' url = 'http://api.vk.com/api.php' param_list = sorted(list(item + '=' + data[item] for item in data)) - data['sig'] = md5(''.join(param_list) + secret).hexdigest() + data['sig'] = md5( + (''.join(param_list) + secret).encode('utf-8') + ).hexdigest() else: url = 'https://api.vk.com/method/' + method From c4f6d8980bc8bcae25b847a8f5f55ddeb010bd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 21 Jan 2014 13:44:50 -0200 Subject: [PATCH 104/890] Add pixelpin to backends index --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index b65a0888b..5ea632d9b 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -78,6 +78,7 @@ Social backends odnoklassnikiru openstreetmap persona + pixelpin pocket podio rdio From 9a0b3752e2c02bab9c5e6187f6b51e14c067ba66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 23 Jan 2014 03:32:22 -0200 Subject: [PATCH 105/890] Use response encoding only when available. Refs #173 --- social/backends/oauth.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 19e1ff873..222a7793c 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -171,7 +171,13 @@ def unauthorized_token(self): callback_uri=self.redirect_uri, decoding=decoding), method=self.REQUEST_TOKEN_METHOD) - return response.content.decode(response.encoding) + content = response.content + if response.encoding or response.apparent_encoding: + content = content.decode(response.encoding or + response.apparent_encoding) + else: + content = response.content.decode() + return content def oauth_authorization_request(self, token): """Generate OAuth request to authorize token.""" From d5efa6d4cdc56110a2cccab0074ae04aeb43529d Mon Sep 17 00:00:00 2001 From: "Michisu, Toshikazu" Date: Sat, 1 Feb 2014 10:18:52 +0900 Subject: [PATCH 106/890] Add version parameter to foursquare backend --- social/backends/foursquare.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/backends/foursquare.py b/social/backends/foursquare.py index 67d4672ce..84203720e 100644 --- a/social/backends/foursquare.py +++ b/social/backends/foursquare.py @@ -10,6 +10,7 @@ class FoursquareOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://foursquare.com/oauth2/authenticate' ACCESS_TOKEN_URL = 'https://foursquare.com/oauth2/access_token' ACCESS_TOKEN_METHOD = 'POST' + API_VERSION = '20140128' def get_user_id(self, details, response): return response['response']['user']['id'] @@ -27,4 +28,5 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://api.foursquare.com/v2/users/self', - params={'oauth_token': access_token}) + params={'oauth_token': access_token, + 'v': self.API_VERSION}) From cf23c3cc416d5f18dd8d09d6b77a3cabe70d2d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 3 Feb 2014 02:54:16 -0200 Subject: [PATCH 107/890] Exclude sure broken version 1.2.4 --- social/tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index c0fcdcc6a..dddda06c6 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,4 +3,4 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -sure>=1.2.0 +sure>=1.2.0,!=1.2.4 From 2d02afbad3a9de1934ab402741e89ab6e8badb4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 5 Feb 2014 11:56:10 -0200 Subject: [PATCH 108/890] Case insensitive query on django. Closes #179 --- social/storage/django_orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 31f10cfa2..f524cb302 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -67,7 +67,7 @@ def get_user(cls, pk): @classmethod def get_users_by_email(cls, email): - return cls.user_model().objects.filter(email=email) + return cls.user_model().objects.filter(email_iexact=email) @classmethod def get_social_auth(cls, provider, uid): From 4a1085f80fa84c652feb022b3fce950b7cea27a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 5 Feb 2014 12:01:50 -0200 Subject: [PATCH 109/890] Restore BackendWrapper to avoid session issues (this backend is deprecated). Refs #128 --- social/apps/django_app/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/social/apps/django_app/utils.py b/social/apps/django_app/utils.py index 4ff7c7912..6d5697eda 100644 --- a/social/apps/django_app/utils.py +++ b/social/apps/django_app/utils.py @@ -52,3 +52,12 @@ def setting(name, default=None): return getattr(settings, setting_name(name)) except AttributeError: return getattr(settings, name, default) + + +class BackendWrapper(object): + # XXX: Deprecated, restored to avoid session issues + def authenticate(self, *args, **kwargs): + return None + + def get_user(self, user_id): + return Strategy(storage=Storage).get_user(user_id) From 711b892c94efabf2784157107b7ae5e122b21b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 5 Feb 2014 20:29:25 -0200 Subject: [PATCH 110/890] Fix iexact field lookup. Refs #179 --- social/storage/django_orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index f524cb302..9c9505a5f 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -67,7 +67,7 @@ def get_user(cls, pk): @classmethod def get_users_by_email(cls, email): - return cls.user_model().objects.filter(email_iexact=email) + return cls.user_model().objects.filter(email__iexact=email) @classmethod def get_social_auth(cls, provider, uid): From 57196b9be619b2c1044bd94613b840ea9d37c961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 5 Feb 2014 21:21:50 -0200 Subject: [PATCH 111/890] v0.1.21 --- Changelog | 64 ++++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 58d42c98f..58fa5cf91 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,64 @@ +2014-02-05 v0.1.21 +================== + + * 2014-02-05 Matías Aguirre + Fix iexact field lookup. Refs #179 + + * 2014-02-05 Matías Aguirre + Restore BackendWrapper to avoid session issues (this backend is + deprecated). Refs #128 + + * 2014-02-05 Matías Aguirre + Case insensitive query on django. Closes #179 + + * 2014-02-03 Matías Aguirre + Exclude sure broken version 1.2.4 + + * 2014-02-01 Michisu, Toshikazu + Add version parameter to foursquare backend + + * 2014-01-23 Matías Aguirre + Use response encoding only when available. Refs #173 + + * 2014-01-21 Matías Aguirre + Add pixelpin to backends index + + * 2014-01-21 Matías Aguirre + Ensure encode() before md5 call for python3. Closes #168 + + * 2014-01-21 lukos + Added PixelPin to list of providers + + * 2014-01-21 Matías Aguirre + PEP8, file formats and line lengths fixes + + * 2014-01-21 luke + Add documentation for PixelPin + + * 2014-01-21 luke + Added new PixelPin provider. + + * 2014-01-20 Matías Aguirre + Use same DB name as other examples + + * 2014-01-20 Yasin Aktimur + Serializer changed. + + * 2014-01-18 Matías Aguirre + Support Weibo domain as username by setting. Closes #164 + + * 2014-01-18 Matías Aguirre + Snippet to get people from circles on Google+ + + * 2014-01-17 Matías Aguirre + Override get_user_id on tumblr backend. Refs #136 + 2014-01-17 v0.1.20 ================== + * 2014-01-17 Matías Aguirre + v0.1.20 + * 2014-01-17 Matías Aguirre Decode bytes on Python3 otherwise it breaks session saving on Django. Refs #139 @@ -398,6 +456,12 @@ * 2013-11-06 Matías Aguirre Ensure IDs to openid association removal. Closes #76 + * 2013-11-05 Branden Rolston + Update partial from session with newer kwargs. + + * 2013-11-05 Branden Rolston + Use mock. + * 2013-11-05 Matías Aguirre Link to tornado docs diff --git a/social/__init__.py b/social/__init__.py index 6160bd18b..fa1b6d101 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 20) +version = (0, 1, 21) extra = '' __version__ = '.'.join(map(str, version)) + extra From f023248ccb401587722bf6571aff4e9dc4d0cae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 9 Feb 2014 21:43:59 -0200 Subject: [PATCH 112/890] Fix LinkedIn OAuth2 backend, pass access token parameter in querystring. Closes #181 --- examples/django_example/example/templates/home.html | 1 + social/backends/linkedin.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index b89938445..953992d02 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -29,6 +29,7 @@ Instagram OAuth2
Jawbone OAuth2
LinkedIn OAuth1
+LinkedIn OAuth2
Live OAuth2
Mail.ru OAuth2
Mendeley OAuth1
diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index 4821d8c9e..fb56731b5 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -83,3 +83,11 @@ def user_data(self, access_token, *args, **kwargs): 'format': 'json'}, headers=self.user_data_headers() ) + + def request_access_token(self, *args, **kwargs): + # LinkedIn expects a POST request with querystring parameters, despite + # the spec http://tools.ietf.org/html/rfc6749#section-4.1.3 + kwargs['params'] = kwargs.pop('data') + return super(LinkedinOAuth2, self).request_access_token( + *args, **kwargs + ) From ce72bd118fd47beafcb69102f69b252899ba30ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 9 Feb 2014 21:59:14 -0200 Subject: [PATCH 113/890] Update sure to 1.2.5 --- social/tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index dddda06c6..8cd53efa9 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,4 +3,4 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -sure>=1.2.0,!=1.2.4 +sure==1.2.5 From ceca57ea8ba37ed2d3a437a7b10e8f6096c4755f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 9 Feb 2014 22:40:02 -0200 Subject: [PATCH 114/890] Stick with sure 1.2.3 (higher is broken, I should drop sure) --- social/tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index 8cd53efa9..0fd266386 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,4 +3,4 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -sure==1.2.5 +sure==1.2.3 From e0522c80b48c4160812afd5334f347ec6b61e6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 00:08:41 -0200 Subject: [PATCH 115/890] Parse token if it's an string (keep a compatible API). Refs #180 --- social/backends/oauth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 222a7793c..d5b04fb7b 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -119,6 +119,8 @@ def auth_complete(self, *args, **kwargs): def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" + if not isinstance(access_token, dict): + access_token = parse_qs(access_token) data = self.user_data(access_token) if data is not None and 'access_token' not in data: data['access_token'] = access_token From e1b0482c944d23f1cae56aab38a981c0a5e17827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 00:22:47 -0200 Subject: [PATCH 116/890] Raise social-auth exception on connection error. Closes #155 --- social/backends/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/social/backends/base.py b/social/backends/base.py index ab9a033a7..8edd0b1e4 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -1,6 +1,7 @@ -from requests import request +from requests import request, ConnectionError from social.utils import module_member, parse_qs +from social.exceptions import AuthFailed class BaseAuth(object): @@ -183,7 +184,10 @@ def uses_redirect(self): def request(self, url, method='GET', *args, **kwargs): kwargs.setdefault('timeout', self.setting('REQUESTS_TIMEOUT') or self.setting('URLOPEN_TIMEOUT')) - response = request(method, url, *args, **kwargs) + try: + response = request(method, url, *args, **kwargs) + except ConnectionError as err: + raise AuthFailed(self, str(err)) response.raise_for_status() return response From 7e6b9341a9563977602d28c9d0447557805fb610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 00:29:02 -0200 Subject: [PATCH 117/890] Fix AuthFailed calls --- social/backends/github.py | 3 ++- social/backends/odnoklassniki.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/social/backends/github.py b/social/backends/github.py index 10feeec0b..8d452296b 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -68,5 +68,6 @@ def user_data(self, access_token, *args, **kwargs): # if the user is a member of the organization, response code # will be 204, see http://bit.ly/ZS6vFl if err.response.status_code != 204: - raise AuthFailed('User doesn\'t belong to the organization') + raise AuthFailed(self, + 'User doesn\'t belong to the organization') return user_data diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 866a40ff0..153db1362 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -84,7 +84,7 @@ def auth_complete(self, request, user, *args, **kwargs): details['extra_data_list'] = fields + auth_data_fields kwargs.update({'backend': self, 'response': details}) else: - raise AuthFailed('Cannot get user details: API error') + raise AuthFailed(self, 'Cannot get user details: API error') return self.strategy.authenticate(*args, **kwargs) def get_auth_sig(self): From c7cc2c50665bfa0f64cf5e5b85a2327bc790e4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 02:40:53 -0200 Subject: [PATCH 118/890] Mendeley OAuth2 backend --- social/backends/mendeley.py | 45 +++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index eb87200eb..17738350c 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -2,14 +2,10 @@ Mendeley OAuth1 backend, docs at: http://psa.matiasaguirre.net/docs/backends/mendeley.html """ -from social.backends.oauth import BaseOAuth1 +from social.backends.oauth import BaseOAuth1, BaseOAuth2 -class MendeleyOAuth(BaseOAuth1): - name = 'mendeley' - AUTHORIZATION_URL = 'http://api.mendeley.com/oauth/authorize/' - REQUEST_TOKEN_URL = 'http://api.mendeley.com/oauth/request_token/' - ACCESS_TOKEN_URL = 'http://api.mendeley.com/oauth/access_token/' +class MendeleyMixin(object): SCOPE_SEPARATOR = '+' EXTRA_DATA = [('profile_id', 'profile_id'), ('name', 'name'), @@ -29,9 +25,40 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Return user data provided""" - values = self.get_json( + values = self.get_user_data(access_token) + values.update(values['main']) + return values + + def get_user_data(self, access_token): + raise NotImplementedError('Implement in subclass') + + +class MendeleyOAuth(BaseOAuth1, MendeleyMixin): + name = 'mendeley' + AUTHORIZATION_URL = 'http://api.mendeley.com/oauth/authorize/' + REQUEST_TOKEN_URL = 'http://api.mendeley.com/oauth/request_token/' + ACCESS_TOKEN_URL = 'http://api.mendeley.com/oauth/access_token/' + + def get_user_data(self, access_token): + return self.get_json( 'http://api.mendeley.com/oapi/profiles/info/me/', auth=self.oauth_auth(access_token) ) - values.update(values['main']) - return values + + +class MendeleyOAuth2(BaseOAuth2, MendeleyMixin): + name = 'mendeley-oauth2' + AUTHORIZATION_URL = 'http://api-oauth2.mendeley.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://api-oauth2.mendeley.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + DEFAULT_SCOPE = ['all'] + EXTRA_DATA = MendeleyMixin.EXTRA_DATA + [ + ('refresh_token', 'refresh_token') + ] + + def get_user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'http://api-oauth2.mendeley.com/oapi/profiles/info/me/', + headers={'Authorization': 'Bearer {0}'.format(access_token)} + ) From bc00c9ac0bbdd2b2e772272c9b9dd725a1dcc3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 02:51:25 -0200 Subject: [PATCH 119/890] Mendeley OAuth2 docs and thanks to Sebastian Bassi (initial author) --- docs/backends/mendeley.rst | 27 ++++++++++++++++++++++++++- docs/thanks.rst | 2 ++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/backends/mendeley.rst b/docs/backends/mendeley.rst index eebcb0c1b..fe2c79da3 100644 --- a/docs/backends/mendeley.rst +++ b/docs/backends/mendeley.rst @@ -1,7 +1,15 @@ Mendeley ======== -Mendeley works with OAuth1, in order to enable the backend follow: +Mendeley supports OAuth1 and OAuth2, they are in the process of deprecating +OAuth1 API (which should be fully deprecated on April 2014, check their +announcement_). + + +OAuth1 +------ + +In order to support OAuth1 (not recomended, use OAuth2 instead): - Register a new application at `Mendeley Application Registration`_ @@ -10,4 +18,21 @@ Mendeley works with OAuth1, in order to enable the backend follow: SOCIAL_AUTH_MENDELEY_KEY = '' SOCIAL_AUTH_MENDELEY_SECRET = '' + +OAuth2 +------ + +In order to support OAuth2: + +- Register a new application at `Mendeley Application Registration`_, or + migrate your OAuth1 application, check their `migration steps here`_. + +- Fill **Application ID** and **Application Secret** values:: + + SOCIAL_AUTH_MENDELEY_OAUTH2_KEY = '' + SOCIAL_AUTH_MENDELEY_OAUTH2_SECRET = '' + + .. _Mendeley Application Registration: http://dev.mendeley.com/applications/register/ +.. _announcement: https://sites.google.com/site/mendeleyapi/home/authentication +.. _migration steps here: https://groups.google.com/forum/#!topic/mendeley-open-api-developers/KmUQW9I0ST0 diff --git a/docs/thanks.rst b/docs/thanks.rst index 10d132655..41678699c 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -108,6 +108,7 @@ let me know and I'll update the list): * spstpl_ * bluszcz_ * vbsteven_ + * sbassi_ .. _python-social-auth: https:https://github.com/https://github.com/github.comhttps://github.com/omabhttps://github.com/python-social-auth @@ -211,3 +212,4 @@ let me know and I'll update the list): .. _spstpl: https://github.com/spstpl .. _bluszcz: https://github.com/bluszcz .. _vbsteven: https://github.com/vbsteven +.. _sbassi: https://github.com/sbassi From b79b3a7846b119ca28bd6ab254b13443aa16a708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 06:58:43 -0200 Subject: [PATCH 120/890] Finishe CherryPy app support and add example application --- examples/cherrypy_example/__init__.py | 75 +++++++++++++++++ examples/cherrypy_example/db/__init__.py | 4 + examples/cherrypy_example/db/saplugin.py | 39 +++++++++ examples/cherrypy_example/db/satool.py | 24 ++++++ examples/cherrypy_example/db/user.py | 16 ++++ .../local_settings.py.template | 43 ++++++++++ examples/cherrypy_example/models.py | 0 examples/cherrypy_example/syncbd.py | 24 ++++++ examples/cherrypy_example/templates/base.html | 25 ++++++ examples/cherrypy_example/templates/done.html | 23 +++++ examples/cherrypy_example/templates/home.html | 83 +++++++++++++++++++ social/apps/cherrypy_app/models.py | 11 +-- social/apps/cherrypy_app/utils.py | 11 ++- social/apps/cherrypy_app/views.py | 21 ++++- social/strategies/cherrypy_strategy.py | 10 +-- 15 files changed, 393 insertions(+), 16 deletions(-) create mode 100644 examples/cherrypy_example/db/__init__.py create mode 100644 examples/cherrypy_example/db/saplugin.py create mode 100644 examples/cherrypy_example/db/satool.py create mode 100644 examples/cherrypy_example/db/user.py create mode 100644 examples/cherrypy_example/local_settings.py.template delete mode 100644 examples/cherrypy_example/models.py create mode 100644 examples/cherrypy_example/syncbd.py create mode 100644 examples/cherrypy_example/templates/base.html create mode 100644 examples/cherrypy_example/templates/done.html create mode 100644 examples/cherrypy_example/templates/home.html diff --git a/examples/cherrypy_example/__init__.py b/examples/cherrypy_example/__init__.py index e69de29bb..a6a39c8de 100644 --- a/examples/cherrypy_example/__init__.py +++ b/examples/cherrypy_example/__init__.py @@ -0,0 +1,75 @@ +import sys + +sys.path.append('../..') + +import cherrypy + +from jinja2 import Environment, FileSystemLoader + +from social.apps.cherrypy_app.utils import backends +from social.apps.cherrypy_app.views import CherryPyPSAViews + +from db.saplugin import SAEnginePlugin +from db.satool import SATool +from db.user import User + + +SAEnginePlugin(cherrypy.engine, 'sqlite:///test.db').subscribe() + + +class PSAExample(CherryPyPSAViews): + @cherrypy.expose + def index(self): + return self.render_to('home.html') + + @cherrypy.expose + def done(self): + user = getattr(cherrypy.request, 'user', None) + return self.render_to('done.html', user=user, backends=backends(user)) + + @cherrypy.expose + def logout(self): + raise cherrypy.HTTPRedirect('/') + + def render_to(self, tpl, **ctx): + return cherrypy.tools.jinja2env.get_template(tpl).render(**ctx) + + +def load_user(): + user_id = cherrypy.session.get('user_id') + if user_id: + cherrypy.request.user = cherrypy.request.db.query(User).get(user_id) + else: + cherrypy.request.user = None + + +def session_commit(): + cherrypy.session.save() + + +try: + from local_settings import SOCIAL_SETTINGS +except ImportError: + print 'Define a local_settings.py using local_settings.py.template as base' + SOCIAL_SETTINGS = {} + + +if __name__ == '__main__': + cherrypy.config.update({ + 'server.socket_port': 8000, + 'tools.sessions.on': True, + 'tools.sessions.storage_type': 'ram', + 'tools.db.on': True, + 'tools.authenticate.on': True, + 'SOCIAL_AUTH_USER_MODEL': 'db.user.User', + 'SOCIAL_AUTH_LOGIN_URL': '/', + 'SOCIAL_AUTH_LOGIN_REDIRECT_URL': '/done', + }) + cherrypy.config.update(SOCIAL_SETTINGS) + cherrypy.tools.jinja2env = Environment( + loader=FileSystemLoader('templates') + ) + cherrypy.tools.db = SATool() + cherrypy.tools.authenticate = cherrypy.Tool('before_handler', load_user) + cherrypy.tools.session = cherrypy.Tool('on_end_resource', session_commit) + cherrypy.quickstart(PSAExample()) diff --git a/examples/cherrypy_example/db/__init__.py b/examples/cherrypy_example/db/__init__.py new file mode 100644 index 000000000..00ea8e136 --- /dev/null +++ b/examples/cherrypy_example/db/__init__.py @@ -0,0 +1,4 @@ +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() diff --git a/examples/cherrypy_example/db/saplugin.py b/examples/cherrypy_example/db/saplugin.py new file mode 100644 index 000000000..072e56d90 --- /dev/null +++ b/examples/cherrypy_example/db/saplugin.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from cherrypy.process import plugins + +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker + + +class SAEnginePlugin(plugins.SimplePlugin): + def __init__(self, bus, connection_string=None): + self.sa_engine = None + self.connection_string = connection_string + self.session = scoped_session(sessionmaker(autoflush=True, + autocommit=False)) + super(SAEnginePlugin, self).__init__(bus) + + def start(self): + self.sa_engine = create_engine(self.connection_string, echo=False) + self.bus.subscribe('bind-session', self.bind) + self.bus.subscribe('commit-session', self.commit) + + def stop(self): + self.bus.unsubscribe('bind-session', self.bind) + self.bus.unsubscribe('commit-session', self.commit) + if self.sa_engine: + self.sa_engine.dispose() + self.sa_engine = None + + def bind(self): + self.session.configure(bind=self.sa_engine) + return self.session + + def commit(self): + try: + self.session.commit() + except: + self.session.rollback() + raise + finally: + self.session.remove() diff --git a/examples/cherrypy_example/db/satool.py b/examples/cherrypy_example/db/satool.py new file mode 100644 index 000000000..1795cd569 --- /dev/null +++ b/examples/cherrypy_example/db/satool.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +import cherrypy + + +class SATool(cherrypy.Tool): + def __init__(self): + super(SATool, self).__init__('before_handler', self.bind_session, + priority=20) + + def _setup(self): + super(SATool, self)._setup() + cherrypy.request.hooks.attach('on_end_resource', + self.commit_transaction, + priority=80) + + def bind_session(self): + session = cherrypy.engine.publish('bind-session').pop() + cherrypy.request.db = session + + def commit_transaction(self): + if not hasattr(cherrypy.request, 'db'): + return + cherrypy.request.db = None + cherrypy.engine.publish('commit-session') diff --git a/examples/cherrypy_example/db/user.py b/examples/cherrypy_example/db/user.py new file mode 100644 index 000000000..94e0c191e --- /dev/null +++ b/examples/cherrypy_example/db/user.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, Integer, String, Boolean + +from db import Base + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + username = Column(String(200)) + password = Column(String(200), default='') + name = Column(String(100)) + email = Column(String(200)) + active = Column(Boolean, default=True) + + def is_active(self): + return self.active diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template new file mode 100644 index 000000000..a988d8286 --- /dev/null +++ b/examples/cherrypy_example/local_settings.py.template @@ -0,0 +1,43 @@ +SOCIAL_SETTINGS = { + 'SOCIAL_AUTH_AUTHENTICATION_BACKENDS': ( + 'social.backends.open_id.OpenIdAuth', + 'social.backends.google.GoogleOpenId', + 'social.backends.google.GoogleOAuth2', + 'social.backends.google.GoogleOAuth', + 'social.backends.twitter.TwitterOAuth', + 'social.backends.yahoo.YahooOpenId', + 'social.backends.stripe.StripeOAuth2', + 'social.backends.persona.PersonaAuth', + 'social.backends.facebook.FacebookOAuth2', + 'social.backends.facebook.FacebookAppOAuth2', + 'social.backends.yahoo.YahooOAuth', + 'social.backends.angel.AngelOAuth2', + 'social.backends.behance.BehanceOAuth2', + 'social.backends.bitbucket.BitbucketOAuth', + 'social.backends.box.BoxOAuth2', + 'social.backends.linkedin.LinkedinOAuth', + 'social.backends.github.GithubOAuth2', + 'social.backends.foursquare.FoursquareOAuth2', + 'social.backends.instagram.InstagramOAuth2', + 'social.backends.live.LiveOAuth2', + 'social.backends.vk.VKOAuth2', + 'social.backends.dailymotion.DailymotionOAuth2', + 'social.backends.disqus.DisqusOAuth2', + 'social.backends.dropbox.DropboxOAuth', + 'social.backends.evernote.EvernoteSandboxOAuth', + 'social.backends.fitbit.FitbitOAuth', + 'social.backends.flickr.FlickrOAuth', + 'social.backends.livejournal.LiveJournalOpenId', + 'social.backends.soundcloud.SoundcloudOAuth2', + 'social.backends.thisismyjam.ThisIsMyJamOAuth1', + 'social.backends.stocktwits.StocktwitsOAuth2', + 'social.backends.tripit.TripItOAuth', + 'social.backends.twilio.TwilioAuth', + 'social.backends.xing.XingOAuth', + 'social.backends.yandex.YandexOAuth2', + 'social.backends.podio.PodioOAuth2', + 'social.backends.reddit.RedditOAuth2', + ), + 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': '', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': '' +} diff --git a/examples/cherrypy_example/models.py b/examples/cherrypy_example/models.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/cherrypy_example/syncbd.py b/examples/cherrypy_example/syncbd.py new file mode 100644 index 000000000..50bb962eb --- /dev/null +++ b/examples/cherrypy_example/syncbd.py @@ -0,0 +1,24 @@ +import sys + +sys.path.append('../..') + +from sqlalchemy import create_engine + +import cherrypy + + +cherrypy.config.update({ + 'SOCIAL_AUTH_USER_MODEL': 'db.user.User', +}) + + +from social.apps.cherrypy_app.models import SocialBase +from db import Base +from db.user import User + + + +if __name__ == '__main__': + engine = create_engine('sqlite:///test.db') + Base.metadata.create_all(engine) + SocialBase.metadata.create_all(engine) diff --git a/examples/cherrypy_example/templates/base.html b/examples/cherrypy_example/templates/base.html new file mode 100644 index 000000000..db47aa234 --- /dev/null +++ b/examples/cherrypy_example/templates/base.html @@ -0,0 +1,25 @@ + + + + Social + + + + {% block content %}{% endblock %} + {% block scripts %}{% endblock %} + +
+ + + + + diff --git a/examples/cherrypy_example/templates/done.html b/examples/cherrypy_example/templates/done.html new file mode 100644 index 000000000..61210838e --- /dev/null +++ b/examples/cherrypy_example/templates/done.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} +

You are logged in as {{ user.username }}!

+ +

Associated:

+
    + {% for assoc in backends.associated %} +
  • + {{ assoc.provider }} (Disconnect) +
  • + {% endfor %} +
+ +

Associate:

+
    + {% for name in backends.not_associated %} +
  • + {{ name }} +
  • + {% endfor %} +
+{% endblock %} diff --git a/examples/cherrypy_example/templates/home.html b/examples/cherrypy_example/templates/home.html new file mode 100644 index 000000000..072375dd6 --- /dev/null +++ b/examples/cherrypy_example/templates/home.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block content %} +Google OAuth2
+Google OAuth
+Google OpenId
+Twitter OAuth
+Yahoo OpenId
+Yahoo OAuth
+Stripe OAuth2
+Facebook OAuth2
+Facebook App
+Angel OAuth2
+Behance OAuth2
+Bitbucket OAuth
+Box OAuth2
+LinkedIn OAuth
+Github OAuth2
+Foursquare OAuth2
+Instagram OAuth2
+Live OAuth2
+VK.com OAuth2
+Dailymotion OAuth2
+Disqus OAuth2
+Dropbox OAuth
+Evernote OAuth (sandbox mode)
+Fitbit OAuth
+Flickr OAuth
+Soundcloud OAuth2
+ThisIsMyJam OAuth1
+Stocktwits OAuth2
+Tripit OAuth
+Twilio
+Xing OAuth
+Yandex OAuth2
+Podio OAuth2
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+ + Persona +
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} diff --git a/social/apps/cherrypy_app/models.py b/social/apps/cherrypy_app/models.py index b23ed7e94..7c80da2dd 100644 --- a/social/apps/cherrypy_app/models.py +++ b/social/apps/cherrypy_app/models.py @@ -16,17 +16,18 @@ SocialBase = declarative_base() +DB_SESSION_ATTR = cherrypy.config.get(setting_name('DB_SESSION_ATTR'), 'db') UID_LENGTH = cherrypy.config.get(setting_name('UID_LENGTH'), 255) User = module_member(cherrypy.config[setting_name('USER_MODEL')]) -class CherryPySocialBase(SocialBase): +class CherryPySocialBase(object): @classmethod def _session(cls): - return cherrypy.request.app.config['db_session'] + return getattr(cherrypy.request, DB_SESSION_ATTR) -class UserSocialAuth(SQLAlchemyUserMixin, CherryPySocialBase): +class UserSocialAuth(CherryPySocialBase, SQLAlchemyUserMixin, SocialBase): """Social Auth association model""" __tablename__ = 'social_auth_usersocialauth' __table_args__ = (UniqueConstraint('provider', 'uid'),) @@ -47,7 +48,7 @@ def user_model(cls): return User -class Nonce(SQLAlchemyNonceMixin, CherryPySocialBase): +class Nonce(CherryPySocialBase, SQLAlchemyNonceMixin, SocialBase): """One use numbers""" __tablename__ = 'social_auth_nonce' __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) @@ -57,7 +58,7 @@ class Nonce(SQLAlchemyNonceMixin, CherryPySocialBase): salt = Column(String(40)) -class Association(SQLAlchemyAssociationMixin, CherryPySocialBase): +class Association(CherryPySocialBase, SQLAlchemyAssociationMixin, SocialBase): """OpenId account association""" __tablename__ = 'social_auth_association' __table_args__ = (UniqueConstraint('server_url', 'handle'),) diff --git a/social/apps/cherrypy_app/utils.py b/social/apps/cherrypy_app/utils.py index c7a685ae6..5196a5f9e 100644 --- a/social/apps/cherrypy_app/utils.py +++ b/social/apps/cherrypy_app/utils.py @@ -4,6 +4,7 @@ from social.utils import setting_name, module_member from social.strategies.utils import get_strategy +from social.backends.utils import user_backends_data DEFAULTS = { @@ -13,8 +14,7 @@ def get_helper(name, do_import=False): - config = cherrypy.request.app.config.get(setting_name(name), - DEFAULTS.get(name, None)) + config = cherrypy.config.get(setting_name(name), DEFAULTS.get(name, None)) return do_import and module_member(config) or config @@ -39,3 +39,10 @@ def wrapper(self, backend=None, *args, **kwargs): return func(self, *args, **kwargs) return wrapper return decorator + + +def backends(user): + """Load Social Auth current user data to context under the key 'backends'. + Will return the output of social.backends.utils.user_backends_data.""" + return user_backends_data(user, get_helper('AUTHENTICATION_BACKENDS'), + get_helper('STORAGE', do_import=True)) diff --git a/social/apps/cherrypy_app/views.py b/social/apps/cherrypy_app/views.py index b31fbe665..7cb3d5091 100644 --- a/social/apps/cherrypy_app/views.py +++ b/social/apps/cherrypy_app/views.py @@ -1,14 +1,27 @@ +import cherrypy + +from social.utils import setting_name, module_member from social.actions import do_auth, do_complete, do_disconnect +from social.apps.cherrypy_app.utils import strategy class CherryPyPSAViews(object): - def auth(self, backend): + @cherrypy.expose + @strategy('/complete/%(backend)s') + def login(self, backend): return do_auth(self.strategy) + @cherrypy.expose + @strategy('/complete/%(backend)s') def complete(self, backend, *args, **kwargs): - # TODO: pass login and pass current user - return do_complete(self.strategy, *args, **kwargs) + login = cherrypy.config.get(setting_name('LOGIN_METHOD')) + do_login = module_member(login) if login else self.do_login + user = getattr(cherrypy.request, 'user', None) + return do_complete(self.strategy, do_login, user=user, *args, **kwargs) + @cherrypy.expose def disconnect(self, backend, association_id=None): - # TODO: pass current user return do_disconnect(self.strategy, association_id) + + def do_login(self, strategy, user): + strategy.session_set('user_id', user.id) diff --git a/social/strategies/cherrypy_strategy.py b/social/strategies/cherrypy_strategy.py index 4869f3272..4f7eb3bd2 100644 --- a/social/strategies/cherrypy_strategy.py +++ b/social/strategies/cherrypy_strategy.py @@ -7,7 +7,7 @@ class CherryPyJinja2TemplateStrategy(BaseTemplateStrategy): def __init__(self, strategy): self.strategy = strategy - self.env = self.strategy.get_setting('jinja2env') + self.env = cherrypy.tools.jinja2env def render_template(self, tpl, context): return self.env.get_template(tpl).render(context) @@ -16,13 +16,13 @@ def render_string(self, html, context): return self.env.from_string(html).render(context) -class CherryPyStratety(BaseStrategy): +class CherryPyStrategy(BaseStrategy): def __init__(self, *args, **kwargs): kwargs.setdefault('tpl', CherryPyJinja2TemplateStrategy) - return super(CherryPyStratety, self).__init__(*args, **kwargs) + return super(CherryPyStrategy, self).__init__(*args, **kwargs) def get_setting(self, name): - return cherrypy.request.app.config[name] + return cherrypy.config[name] def request_data(self, merge=True): if merge: @@ -37,7 +37,7 @@ def request_host(self): return cherrypy.request.base def redirect(self, url): - return cherrypy.HTTPRedirect(url) + raise cherrypy.HTTPRedirect(url) def html(self, content): return content From ab11baffb75682f094c8a2525d4519b3b56b86c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 17:50:27 -0200 Subject: [PATCH 121/890] Switch parent class to avoid overrides --- social/backends/mendeley.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index 17738350c..9e7f9c978 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -33,7 +33,7 @@ def get_user_data(self, access_token): raise NotImplementedError('Implement in subclass') -class MendeleyOAuth(BaseOAuth1, MendeleyMixin): +class MendeleyOAuth(MendeleyMixin, BaseOAuth1): name = 'mendeley' AUTHORIZATION_URL = 'http://api.mendeley.com/oauth/authorize/' REQUEST_TOKEN_URL = 'http://api.mendeley.com/oauth/request_token/' @@ -46,7 +46,7 @@ def get_user_data(self, access_token): ) -class MendeleyOAuth2(BaseOAuth2, MendeleyMixin): +class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): name = 'mendeley-oauth2' AUTHORIZATION_URL = 'http://api-oauth2.mendeley.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://api-oauth2.mendeley.com/oauth/token' From db65a75ece0276047d0cd75f35c343ad2c245865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 21:41:50 -0200 Subject: [PATCH 122/890] Fix Mendeley OAuth2 implementation, use https URLs --- social/backends/mendeley.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index 9e7f9c978..ccd10ba63 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -48,7 +48,7 @@ def get_user_data(self, access_token): class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): name = 'mendeley-oauth2' - AUTHORIZATION_URL = 'http://api-oauth2.mendeley.com/oauth/authorize' + AUTHORIZATION_URL = 'https://api-oauth2.mendeley.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://api-oauth2.mendeley.com/oauth/token' ACCESS_TOKEN_METHOD = 'POST' DEFAULT_SCOPE = ['all'] @@ -59,6 +59,6 @@ class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): def get_user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( - 'http://api-oauth2.mendeley.com/oapi/profiles/info/me/', + 'https://api-oauth2.mendeley.com/oapi/profiles/info/me/', headers={'Authorization': 'Bearer {0}'.format(access_token)} ) From f7b3893a0fcc3530e7d443dc9d4373d23890197c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 21:42:03 -0200 Subject: [PATCH 123/890] Mendeley OAuth2 in example app --- examples/django_example/example/settings.py | 1 + examples/django_example/example/templates/home.html | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index d577beb26..2aef6fb91 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -152,6 +152,7 @@ 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.mailru.MailruOAuth2', 'social.backends.mendeley.MendeleyOAuth', + 'social.backends.mendeley.MendeleyOAuth2', 'social.backends.mixcloud.MixcloudOAuth2', 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', 'social.backends.open_id.OpenIdAuth', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 953992d02..29c42209e 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -33,6 +33,7 @@ Live OAuth2
Mail.ru OAuth2
Mendeley OAuth1
+Mendeley OAuth2
Mixcloud OAuth2
Odnoklassniki OAuth2
OpenStreetMap OAuth1
From 7ec3ae9a2ae3bac947b781af9e50b548ee8dee8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 21:57:49 -0200 Subject: [PATCH 124/890] Get extra_data from details on openid too --- social/backends/open_id.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index dbe0f8a31..b00797675 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -108,7 +108,12 @@ def extra_data(self, user, uid, response, details): """ sreg_names = self.setting('SREG_EXTRA_DATA') ax_names = self.setting('AX_EXTRA_DATA') - return self.values_from_response(response, sreg_names, ax_names) + values = self.values_from_response(response, sreg_names, ax_names) + from_details = super(OpenIdAuth, self).extra_data( + user, uid, {}, details + ) + values.update(from_details) + return values def auth_url(self): """Return auth URL returned by service""" From 7caf239c8115b68f625bcf2e3351c594c5a58dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 22:09:12 -0200 Subject: [PATCH 125/890] Pass user on disconnect --- social/apps/cherrypy_app/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/apps/cherrypy_app/views.py b/social/apps/cherrypy_app/views.py index 7cb3d5091..a9a4cd23b 100644 --- a/social/apps/cherrypy_app/views.py +++ b/social/apps/cherrypy_app/views.py @@ -21,7 +21,8 @@ def complete(self, backend, *args, **kwargs): @cherrypy.expose def disconnect(self, backend, association_id=None): - return do_disconnect(self.strategy, association_id) + user = getattr(cherrypy.request, 'user', None) + return do_disconnect(self.strategy, user, association_id) def do_login(self, strategy, user): strategy.session_set('user_id', user.id) From d1dda12be4414327644070b264959f81f006f26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 22:09:28 -0200 Subject: [PATCH 126/890] Disconnection on example app --- examples/cherrypy_example/__init__.py | 2 ++ examples/cherrypy_example/templates/done.html | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/cherrypy_example/__init__.py b/examples/cherrypy_example/__init__.py index a6a39c8de..2a69a81a0 100644 --- a/examples/cherrypy_example/__init__.py +++ b/examples/cherrypy_example/__init__.py @@ -25,6 +25,8 @@ def index(self): @cherrypy.expose def done(self): user = getattr(cherrypy.request, 'user', None) + if user is None: + raise cherrypy.HTTPRedirect('/') return self.render_to('done.html', user=user, backends=backends(user)) @cherrypy.expose diff --git a/examples/cherrypy_example/templates/done.html b/examples/cherrypy_example/templates/done.html index 61210838e..9fd7b60c8 100644 --- a/examples/cherrypy_example/templates/done.html +++ b/examples/cherrypy_example/templates/done.html @@ -7,7 +7,8 @@
    {% for assoc in backends.associated %}
  • - {{ assoc.provider }} (Disconnect) + {{ assoc.provider }} +
  • {% endfor %}
From c3da893649d5fd510de34277c91e5356b18c6164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 22:32:54 -0200 Subject: [PATCH 127/890] CherryPy docs --- docs/configuration/cherrypy.rst | 81 +++++++++++++++++++++++++++++++++ docs/configuration/index.rst | 1 + 2 files changed, 82 insertions(+) create mode 100644 docs/configuration/cherrypy.rst diff --git a/docs/configuration/cherrypy.rst b/docs/configuration/cherrypy.rst new file mode 100644 index 000000000..bd72659e7 --- /dev/null +++ b/docs/configuration/cherrypy.rst @@ -0,0 +1,81 @@ +CherryPy Framework +================== + +CherryPy framework is supported, it works but I'm sure there's room for +improvements. The implementation uses SQLAlchemy as ORM and expects some values +accessible on ``cherrypy.request`` for it to work. + +At the moment the configuration is expected on ``cherrypy.config`` but ideally +it should be an application configuration instead. + +Expected values are: + +``cherrypy.request.user`` + Current logged in user, load it in your application on a ``before_handler`` + handler. + +``cherrypy.request.db`` + Current database session, again, load it in your application on + a ``before_handler``. + + +Dependencies +------------ + +The `CherryPy built-in application` depends on sqlalchemy_, there's no support for +others ORMs yet but pull-requests are welcome. + + +Enabling the application +------------------------ + +The application is defined on ``social.apps.cherrypy_app.views.CherryPyPSAViews``, +register it in the preferred way for your project. + +Check the rest of the docs for the other settings like enabling authentication +backends and backends keys. + + +Models Setup +------------ + +The models are located in ``social.apps.cherrypy_app.models``. A reference to +your ``User`` model is required to be defined in the project settings, it +should be an import path, for example:: + + cherrypy.config.update({ + 'SOCIAL_AUTH_USER_MODEL': 'models.User' + }) + + +Login mechanism +--------------- + +By default the application sets the session value ``user_id``, this is a simple +solution and it should be improved, if you want to provider your own login +mechanism you can do it by defining the ``SOCIAL_AUTH_LOGIN_METHOD`` setting, +it should be an import path to a callable, like this:: + + SOCIAL_AUTH_USER_MODEL = 'app.login_user' + +And an example of this function:: + + def login_user(strategy, user): + strategy.session_set('user_id', user.id) + +Then, ensure to load the user in your application at ``cherrypy.request.user``, +for example:: + + def load_user(): + user_id = cherrypy.session.get('user_id') + if user_id: + cherrypy.request.user = cherrypy.request.db.query(User).get(user_id) + else: + cherrypy.request.user = None + + + cherrypy.tools.authenticate = cherrypy.Tool('before_handler', load_user) + + +.. _CherryPy built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/cherrypy_app +.. _sqlalchemy: http://www.sqlalchemy.org/ diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst index 84b6e6c58..3cfa6b735 100644 --- a/docs/configuration/index.rst +++ b/docs/configuration/index.rst @@ -19,5 +19,6 @@ Contents: django flask pyramid + cherrypy webpy porting_from_dsa From d9776e3e713ce8cd48f8a4c21d59aec9be29f003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 10 Feb 2014 22:35:55 -0200 Subject: [PATCH 128/890] CherryPy mention in project index --- site/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/index.html b/site/index.html index 2265ef642..d1e80d2cb 100644 --- a/site/index.html +++ b/site/index.html @@ -46,9 +46,9 @@

Frameworks

The lib supports a few frameworks at the moment with Django, Flask, Pyramid, - Webpy and Tornado and more to come. - The frameworks API should ease the implementation to increase the number - of frameworks supported. + Webpy, CherryPy and + Tornado and more to come. The frameworks API + should ease the implementation to increase the number of frameworks supported.

View details »

From ee291d168a41dc8102e4854d011a2efd6c372af7 Mon Sep 17 00:00:00 2001 From: Hassek Date: Tue, 11 Feb 2014 12:49:55 -0500 Subject: [PATCH 129/890] updated live connection for better support --- social/backends/live.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/social/backends/live.py b/social/backends/live.py index 2884438fb..7317c1667 100644 --- a/social/backends/live.py +++ b/social/backends/live.py @@ -15,13 +15,20 @@ class LiveOAuth2(BaseOAuth2): EXTRA_DATA = [ ('id', 'id'), ('access_token', 'access_token'), - ('reset_token', 'reset_token'), - ('expires', 'expires'), + ('authentication_token', 'authentication_token'), + ('refresh_token', 'refresh_token'), + ('expires_in', 'expires'), ('email', 'email'), ('first_name', 'first_name'), ('last_name', 'last_name'), + ('token_type', 'token_type'), ] + def extra_data(self, user, uid, response, details): + extra_data = super(LiveOAuth2, self).extra_data(user, uid, response, details) + extra_data['email'] = response.get('emails', {}).get('account') + return extra_data + def get_user_details(self, response): """Return user details from Live Connect account""" return {'username': response.get('name'), From 9004081cfcddc0d914c9d1ebdbbbceb1312b17c5 Mon Sep 17 00:00:00 2001 From: Hassek Date: Wed, 12 Feb 2014 09:55:28 -0500 Subject: [PATCH 130/890] removed extra_data override --- social/backends/live.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/social/backends/live.py b/social/backends/live.py index 7317c1667..497daebe3 100644 --- a/social/backends/live.py +++ b/social/backends/live.py @@ -24,11 +24,6 @@ class LiveOAuth2(BaseOAuth2): ('token_type', 'token_type'), ] - def extra_data(self, user, uid, response, details): - extra_data = super(LiveOAuth2, self).extra_data(user, uid, response, details) - extra_data['email'] = response.get('emails', {}).get('account') - return extra_data - def get_user_details(self, response): """Return user details from Live Connect account""" return {'username': response.get('name'), From 21c66179953236ce4252bb03d56fe0226945c378 Mon Sep 17 00:00:00 2001 From: "Joe B. Lewis" Date: Thu, 13 Feb 2014 01:39:51 +0530 Subject: [PATCH 131/890] added information for FIELDS_STORED_IN_SESSION If different code needs to be executed in different instances of 'login' buttons, this might come in handy. And it's found nowhere in the existing docs. --- docs/use_cases.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index ddea4de59..b67effb9e 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -18,6 +18,27 @@ Django:: Login with Facebook +Pass custom GET/POST parameters and retrieve them on authentication +-------------------------------------------------------------------------- + +In some cases, you might need to send data over the url, and retrieve it while processing the after-effect. +For example, for conditionally executing code in custom pipelines. + +In such cases, add it to FIELDS_STORED_IN_SESSION. + +In settings.py,:: + + FIELDS_STORED_IN_SESSION = ['key'] + +In template,:: + + Login with Facebook + +In your custom pipeline, retrieve it using:: + + strategy.session_get('key') + + Retrieve Google+ Friends ------------------------ From 85dca3ebfd5de4f6b4af9ce75b1a385c2a3e881c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 12 Feb 2014 23:28:34 -0200 Subject: [PATCH 132/890] Style recent docs --- docs/use_cases.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index b67effb9e..156c16b54 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -18,19 +18,21 @@ Django:: Login with Facebook + Pass custom GET/POST parameters and retrieve them on authentication --------------------------------------------------------------------------- +------------------------------------------------------------------- -In some cases, you might need to send data over the url, and retrieve it while processing the after-effect. -For example, for conditionally executing code in custom pipelines. +In some cases, you might need to send data over the URL, and retrieve it while +processing the after-effect. For example, for conditionally executing code in +custom pipelines. -In such cases, add it to FIELDS_STORED_IN_SESSION. +In such cases, add it to ``FIELDS_STORED_IN_SESSION``. -In settings.py,:: +In your settings:: FIELDS_STORED_IN_SESSION = ['key'] -In template,:: +In template:: Login with Facebook From c07595d6ea28f0cc25581a339eaf5db360230a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 13 Feb 2014 01:02:47 -0200 Subject: [PATCH 133/890] Docs about associate user by email --- docs/use_cases.rst | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 156c16b54..463e075ca 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -66,5 +66,42 @@ Once we have the ``access token`` we can call the API like this:: friends = response.json()['items'] +Associate users by email +------------------------ + +Sometimes it's desirable that social accounts are automatically associated if +the email already matches a user account. + +For example, if a user signed up with his Facebook account, then logged out and +next time tries to use Google OAuth2 to login, it could be nice (if both social +sites have the same email address configured) that the user gets into his +initial account created by Facebook backend. + +This scenario is possible by enabling the ``associate_by_email`` pipeline +function, like this:: + + SOCIAL_AUTH_PIPELINE = ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.auth_allowed', + 'social.pipeline.social_auth.social_user', + 'social.pipeline.user.get_username', + 'social.pipeline.social_auth.associate_by_email', # <--- enable this one + 'social.pipeline.user.create_user', + 'social.pipeline.social_auth.associate_user', + 'social.pipeline.social_auth.load_extra_data', + 'social.pipeline.user.user_details' + ) + +This feature is disabled by default because it's not 100% secure to automate +this process with all the backends. Not all the providers will validate your +email account and others users could take advantage of that. + +Take for instance User A registered in your site with the email +``foo@bar.com``. Then a malicious user registers into another provider that +doesn't validate his email with that same account. Finally this user will turn +to your site (which supports that provider) and sign up to it, since the email +is the same, the malicious user will take control over the User A account. + .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list From cf686d8f2119815270390a635b0aab1efb0c4879 Mon Sep 17 00:00:00 2001 From: Bitdeli Chef Date: Thu, 13 Feb 2014 03:19:39 +0000 Subject: [PATCH 134/890] Add a Bitdeli badge to README --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 296a07012..6f0562999 100644 --- a/README.rst +++ b/README.rst @@ -276,3 +276,9 @@ check `django-social-auth LICENSE`_ for details: .. _six: http://pythonhosted.org/six/ .. _requests: http://docs.python-requests.org/en/latest/ .. _PixelPin: http://pixelpin.co.uk + + +.. image:: https://d2weczhvl823v0.cloudfront.net/omab/python-social-auth/trend.png + :alt: Bitdeli badge + :target: https://bitdeli.com/free + From f87aba0b8fed8476133d21c69abda1d02ecee1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 13 Feb 2014 01:18:21 -0200 Subject: [PATCH 135/890] Move badge to the top --- README.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 6f0562999..e6945e4da 100644 --- a/README.rst +++ b/README.rst @@ -17,6 +17,10 @@ for more frameworks and ORMs. .. image:: https://pypip.in/d/python-social-auth/badge.png :target: https://crate.io/packages/python-social-auth?version=latest +.. image:: https://d2weczhvl823v0.cloudfront.net/omab/python-social-auth/trend.png + :alt: Bitdeli badge + :target: https://bitdeli.com/free + .. contents:: Table of Contents @@ -276,9 +280,3 @@ check `django-social-auth LICENSE`_ for details: .. _six: http://pythonhosted.org/six/ .. _requests: http://docs.python-requests.org/en/latest/ .. _PixelPin: http://pixelpin.co.uk - - -.. image:: https://d2weczhvl823v0.cloudfront.net/omab/python-social-auth/trend.png - :alt: Bitdeli badge - :target: https://bitdeli.com/free - From 3aec8a973b809746ffdee33de39f55e64b293e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 13 Feb 2014 04:09:16 -0200 Subject: [PATCH 136/890] Vimeo backend --- docs/backends/index.rst | 1 + docs/backends/vimeo.rst | 28 +++++++++++++++ docs/intro.rst | 2 ++ examples/django_example/example/settings.py | 1 + .../example/templates/home.html | 2 +- social/backends/vimeo.py | 34 +++++++++++++++++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 docs/backends/vimeo.rst create mode 100644 social/backends/vimeo.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 5ea632d9b..46da01a14 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -100,6 +100,7 @@ Social backends tumblr twilio twitter + vimeo vk weibo xing diff --git a/docs/backends/vimeo.rst b/docs/backends/vimeo.rst new file mode 100644 index 000000000..b2e4bd089 --- /dev/null +++ b/docs/backends/vimeo.rst @@ -0,0 +1,28 @@ +Vimeo +===== + +Vimeo uses OAuth1 to grant access to their API. In order to get the backend +running follow: + +- Register an application at `Vimeo Developer Portal`_ filling the required + settings. Ensure to fill ``App Callback URL`` field with + ``http:///complete/vimeo/`` + +- Fill in the **Client Id** and **Client Secret** values in your settings:: + + SOCIAL_AUTH_VIMEO_KEY = '' + SOCIAL_AUTH_VIMEO_SECRET = '' + +- Specify scopes with:: + + SOCIAL_AUTH_VIMEO_SCOPE = [...] + +- Add the backend to ``AUTHENTICATION_BACKENDS``:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.vimeo.VimeoOAuth1', + ... + ) + +.. _Vimeo Developer Portal: https://developer.vimeo.com/apps/new diff --git a/docs/intro.rst b/docs/intro.rst index 0cbda6b90..d68844573 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -75,6 +75,7 @@ or extend current one): * Tumblr_ OAuth1 * Twilio_ Auth * Twitter_ OAuth1 + * Vimeo_ OAuth1 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 * Xing_ OAuth1 @@ -152,6 +153,7 @@ section. .. _Stackoverflow: http://stackoverflow.com/ .. _Steam: http://steamcommunity.com/ .. _Rdio: https://www.rdio.com +.. _Vimeo: https://vimeo.com/ .. _Tumblr: http://www.tumblr.com/ .. _Django: https://github.com/omab/python-social-auth/tree/master/social/apps/django_app .. _Flask: https://github.com/omab/python-social-auth/tree/master/social/apps/flask_app diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 2aef6fb91..d0c86cf04 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -185,6 +185,7 @@ 'social.backends.yahoo.YahooOpenId', 'social.backends.yammer.YammerOAuth2', 'social.backends.yandex.YandexOAuth2', + 'social.backends.vimeo.VimeoOAuth1', 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 29c42209e..11f87337c 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -64,8 +64,8 @@ Yahoo OAuth1
Yammer OAuth2
Yandex OAuth2 -
TAOBAO OAuth2
+Vimeo OAuth1
Email Auth
Username Auth
diff --git a/social/backends/vimeo.py b/social/backends/vimeo.py new file mode 100644 index 000000000..a6a61ec2f --- /dev/null +++ b/social/backends/vimeo.py @@ -0,0 +1,34 @@ +from social.backends.oauth import BaseOAuth1 + + +class VimeoOAuth1(BaseOAuth1): + """Vimeo OAuth authentication backend""" + name = 'vimeo' + AUTHORIZATION_URL = 'https://vimeo.com/oauth/authorize' + REQUEST_TOKEN_URL = 'https://vimeo.com/oauth/request_token' + ACCESS_TOKEN_URL = 'https://vimeo.com/oauth/access_token' + + def get_user_id(self, details, response): + return response.get('person', {}).get('id') + + def get_user_details(self, response): + """Return user details from Twitter account""" + person = response.get('person', {}) + fullname = person.get('display_name', '') + if ' ' in fullname: + first_name, last_name = fullname.split(' ', 1) + else: + first_name, last_name = fullname, '' + return {'username': person.get('username', ''), + 'email': '', + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Return user data provided""" + return self.get_json( + 'https://vimeo.com/api/rest/v2', + params={'format': 'json', 'method': 'vimeo.people.getInfo'}, + auth=self.oauth_auth(access_token) + ) From 8adca5842dde00391f5ad4a96a1add1303d24c87 Mon Sep 17 00:00:00 2001 From: Yan Kalchevskiy Date: Fri, 14 Feb 2014 01:25:18 +0700 Subject: [PATCH 137/890] Fixed a typo. --- social/apps/django_app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/__init__.py b/social/apps/django_app/__init__.py index 726eaa10f..1f5b92f98 100644 --- a/social/apps/django_app/__init__.py +++ b/social/apps/django_app/__init__.py @@ -4,7 +4,7 @@ To use this: * Add 'social.apps.django_app.default' if using default ORM, or 'social.apps.django_app.me' if using mongoengine - * Add url('', 'social.apps.django_app.urls') to urls.py + * Add url('', include('social.apps.django_app.urls', namespace='social')) to urls.py * Define SOCIAL_AUTH_STORAGE and SOCIAL_AUTH_STRATEGY, default values: SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' From b10eb466da6f0b5d7e6b39dcc067765c0985fa9a Mon Sep 17 00:00:00 2001 From: Thomas Lovett Date: Thu, 13 Feb 2014 15:02:55 -0500 Subject: [PATCH 138/890] add clef to main README --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index e6945e4da..0ac8ef834 100644 --- a/README.rst +++ b/README.rst @@ -60,6 +60,7 @@ or current ones extended): * BelgiumEIDOpenId_ OpenId https://www.e-contract.be/ * Bitbucket_ OAuth1 * Box_ OAuth2 + * Clef_ OAuth2 * Dailymotion_ OAuth2 * Disqus_ OAuth2 * Douban_ OAuth1 and OAuth2 @@ -214,6 +215,7 @@ check `django-social-auth LICENSE`_ for details: .. _Behance: https://www.behance.net .. _Bitbucket: https://bitbucket.org .. _Box: https://www.box.com +.. _Clef: https://getclef.com/ .. _Dailymotion: https://dailymotion.com .. _Disqus: https://disqus.com .. _Douban: http://www.douban.com From 33584db72321abfab6974abcd859890744bd3c00 Mon Sep 17 00:00:00 2001 From: Thomas Lovett Date: Thu, 13 Feb 2014 15:04:11 -0500 Subject: [PATCH 139/890] fix copy-paste typo in callback url --- docs/backends/clef.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/clef.rst b/docs/backends/clef.rst index c9f33a397..cfdc6393a 100644 --- a/docs/backends/clef.rst +++ b/docs/backends/clef.rst @@ -4,7 +4,7 @@ Clef Clef works similar to Facebook (OAuth). - Register a new application at `Clef Developers`_, set the callback URL to - ``http://example.com/complete/github/`` replacing ``example.com`` with your + ``http://example.com/complete/clef/`` replacing ``example.com`` with your domain. - Fill ``App Id`` and ``App Secret`` values in the settings:: From 4fe9dd56830a0ce4c5ecd896cd09070d8e31a0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 13 Feb 2014 22:48:34 -0200 Subject: [PATCH 140/890] PEP8, Python3 and example fixes --- examples/flask_example/settings.py | 2 +- examples/pyramid_example/example/settings.py | 2 +- examples/tornado_example/settings.py | 2 +- examples/webpy_example/app.py | 2 +- social/backends/clef.py | 21 ++++++-------------- social/tests/backends/test_clef.py | 5 +---- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 542b0b530..f2cd91ad5 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -48,7 +48,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', - 'social.backends.tripit.ClefOAuth2', + 'social.backends.clef.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index f798f5085..ece04853b 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -38,7 +38,7 @@ 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', 'social.backends.twilio.TwilioAuth', - 'social.backends.tripit.ClefOAuth2', + 'social.backends.clef.ClefOAuth2', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index 2181f1b9d..8ac85f9dc 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -36,7 +36,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', - 'social.backends.tripit.ClefOAuth2', + 'social.backends.clef.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index d7a1dd339..bafde9ec8 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -49,7 +49,7 @@ 'social.backends.thisismyjam.ThisIsMyJamOAuth1', 'social.backends.stocktwits.StocktwitsOAuth2', 'social.backends.tripit.TripItOAuth', - 'social.backends.tripit.ClefOAuth2', + 'social.backends.clef.ClefOAuth2', 'social.backends.twilio.TwilioAuth', 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', diff --git a/social/backends/clef.py b/social/backends/clef.py index e2caa76a8..f7e09bdd0 100644 --- a/social/backends/clef.py +++ b/social/backends/clef.py @@ -2,25 +2,20 @@ Clef OAuth support. This contribution adds support for Clef OAuth service. The settings -SOCIAL_AUTH_CLEF_KEY and SOCIAL_AUTH_CLEF_SECRET must be defined with the values -given by Clef application registration process. +SOCIAL_AUTH_CLEF_KEY and SOCIAL_AUTH_CLEF_SECRET must be defined with the +values given by Clef application registration process. """ -import json -import urllib2 from social.backends.oauth import BaseOAuth2 + class ClefOAuth2(BaseOAuth2): """Clef OAuth authentication backend""" name = 'clef' AUTHORIZATION_URL = 'https://clef.io/iframes/qr' ACCESS_TOKEN_URL = 'https://clef.io/api/v1/authorize' - INFO_URL = 'https://clef.io/api/v1/info' - ACCESS_TOKEN_METHOD = "POST" + ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('id', 'id') - ] def auth_params(self, *args, **kwargs): params = super(ClefOAuth2, self).auth_params(*args, **kwargs) @@ -28,7 +23,6 @@ def auth_params(self, *args, **kwargs): params['redirect_url'] = params.pop('redirect_uri') return params - def get_user_details(self, response): """Return user details from Github account""" info = response.get('info') @@ -41,8 +35,5 @@ def get_user_details(self, response): } def user_data(self, access_token, *args, **kwargs): - url = '%s?access_token=%s' % (self.INFO_URL, access_token) - try: - return json.load(urllib2.urlopen(url)) - except ValueError: - return None + return self.get_json('https://clef.io/api/v1/info', + params={'access_token': access_token}) diff --git a/social/tests/backends/test_clef.py b/social/tests/backends/test_clef.py index 277fe969e..d70f9a293 100644 --- a/social/tests/backends/test_clef.py +++ b/social/tests/backends/test_clef.py @@ -1,11 +1,8 @@ import json -from httpretty import HTTPretty - -from social.exceptions import AuthFailed - from social.tests.backends.oauth import OAuth2Test + class ClefOAuth2Test(OAuth2Test): backend_path = 'social.backends.clef.ClefOAuth2' user_data_url = 'https://clef.io/api/v1/info' From f510c3f160ce11f98f2e4be55f59ddc57f3b87f2 Mon Sep 17 00:00:00 2001 From: David Kingman Date: Tue, 18 Feb 2014 10:04:06 -0700 Subject: [PATCH 141/890] Removed commit marker --- examples/django_example/example/templates/home.html | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 3e7ba6aa2..cdc04b742 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -67,7 +67,6 @@ Yandex OAuth2 TAOBAO OAuth2
Vimeo OAuth1
->>>>>>> 3aec8a973b809746ffdee33de39f55e64b293e9a Email Auth
Username Auth
From 0496fd91683a10d56027c4d11a3a2295247dda17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 21 Feb 2014 00:55:12 -0200 Subject: [PATCH 142/890] Dev marker --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index fa1b6d101..e993f2fda 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 21) -extra = '' +version = (0, 1, 22) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 9b3fe234459e755066ef7d1855a7fb45fac5e162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 24 Feb 2014 06:54:14 -0200 Subject: [PATCH 143/890] User USERNAME_FIELD on mongoengine. Closes #197 --- social/apps/django_app/me/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/me/models.py b/social/apps/django_app/me/models.py index de70fe78c..33a484c54 100644 --- a/social/apps/django_app/me/models.py +++ b/social/apps/django_app/me/models.py @@ -71,7 +71,9 @@ def create_social_auth(cls, user, uid, provider): @classmethod def username_max_length(cls): - return UserSocialAuth.user_model().username.max_length + username_field = cls.username_field() + field = getattr(UserSocialAuth.user_model(), username_field) + return field.max_length @classmethod def user_model(cls): From 05c54c86fcaf35e0f39c220c59eac7f5a2af650e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 25 Feb 2014 15:24:19 -0200 Subject: [PATCH 144/890] Add 'user' to default scope on coinbase backend. Closes #199 --- social/backends/coinbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/coinbase.py b/social/backends/coinbase.py index 22546c396..d7fd594ce 100644 --- a/social/backends/coinbase.py +++ b/social/backends/coinbase.py @@ -8,7 +8,7 @@ class CoinbaseOAuth2(BaseOAuth2): name = 'coinbase' SCOPE_SEPARATOR = '+' - DEFAULT_SCOPE = ['balance'] + DEFAULT_SCOPE = ['user', 'balance'] AUTHORIZATION_URL = 'https://coinbase.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://coinbase.com/oauth/token' ACCESS_TOKEN_METHOD = 'POST' From 2aa40fb8174fab573487898e85770afe2c8dee1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Feb 2014 13:03:37 -0200 Subject: [PATCH 145/890] Better error message --- social/pipeline/social_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 2115c05b8..6e317e6dc 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -73,7 +73,7 @@ def associate_by_email(strategy, details, user=None, *args, **kwargs): elif len(users) > 1: raise AuthException( strategy.backend, - 'The given email address is associated with multiple accounts' + 'The given email address is associated with another account' ) else: return {'user': users[0]} From 21f462f18dd1b5917e811ab084e3470ce9969aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Feb 2014 13:03:56 -0200 Subject: [PATCH 146/890] Set is_new flag on pipeline if user is not new. Refs #201 --- social/pipeline/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index eed8662bc..647c5653a 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -55,7 +55,7 @@ def get_username(strategy, details, user=None, *args, **kwargs): def create_user(strategy, details, response, uid, user=None, *args, **kwargs): if user: - return + return {'is_new': False} fields = dict((name, kwargs.get(name) or details.get(name)) for name in strategy.setting('USER_FIELDS', From 11f3558252920e26cea0aa9e336d72b12f9d9fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Feb 2014 13:33:18 -0200 Subject: [PATCH 147/890] Don't update user if it's set to None (non-authenticated pipeline continuation). Refs #198 --- social/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/social/utils.py b/social/utils.py index 0dabc88a6..37415aa99 100644 --- a/social/utils.py +++ b/social/utils.py @@ -124,13 +124,14 @@ def drop_lists(value): return out -def partial_pipeline_data(strategy, user, *args, **kwargs): +def partial_pipeline_data(strategy, user=None, *args, **kwargs): partial = strategy.session_get('partial_pipeline', None) if partial: idx, backend, xargs, xkwargs = strategy.partial_from_session(partial) if backend == strategy.backend.name: kwargs.setdefault('pipeline_index', idx) - kwargs.setdefault('user', user) + if user: # don't update user if it's None + kwargs.setdefault('user', user) kwargs.setdefault('request', strategy.request) xkwargs.update(kwargs) return xargs, xkwargs From de56b10f20269ae4e219ef306db1edceaea216d9 Mon Sep 17 00:00:00 2001 From: Sebastian Bassi Date: Thu, 27 Feb 2014 15:38:14 -0200 Subject: [PATCH 148/890] Update mendeley.py adding extra data as stated here: http://apidocs.mendeley.com/home/authentication --- social/backends/mendeley.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index ccd10ba63..3e15ef21f 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -53,7 +53,9 @@ class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' DEFAULT_SCOPE = ['all'] EXTRA_DATA = MendeleyMixin.EXTRA_DATA + [ - ('refresh_token', 'refresh_token') + ('refresh_token', 'refresh_token'), + ('expires_in', 'expires_in'), + ('token_type', 'token_type'), ] def get_user_data(self, access_token, *args, **kwargs): From 3f9502380ab1f65f00216b81c65cf596d7446dc3 Mon Sep 17 00:00:00 2001 From: Andrey Kuzmin Date: Fri, 28 Feb 2014 22:23:05 +0400 Subject: [PATCH 149/890] Fixes broken email confirmation for SQLAlchemy storage and webpy_app --- social/apps/webpy_app/models.py | 6 +++++- social/storage/sqlalchemy_orm.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/social/apps/webpy_app/models.py b/social/apps/webpy_app/models.py index 597b44942..71be9975a 100644 --- a/social/apps/webpy_app/models.py +++ b/social/apps/webpy_app/models.py @@ -12,7 +12,7 @@ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ BaseSQLAlchemyStorage -from social.apps.flask_app.fields import JSONType +from social.apps.webpy_app.fields import JSONType SocialBase = declarative_base() @@ -84,6 +84,10 @@ class Code(SQLAlchemyCodeMixin, SocialBase): email = Column(String(200)) code = Column(String(32), index=True) + @classmethod + def _session(cls): + return web.db_session + class WebpyStorage(BaseSQLAlchemyStorage): user = UserSocialAuth diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 8fb9bd6d5..839b665e9 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -30,6 +30,9 @@ def _save_instance(cls, instance): cls._session().commit() return instance + def save(self): + self._save_instance(self) + class SQLAlchemyUserMixin(SQLAlchemyMixin, UserMixin): """Social Auth association model""" From c0cb028f6060637b3d21eab16c5bc93ac93c52d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Mar 2014 04:45:08 -0200 Subject: [PATCH 150/890] v0.1.22 --- Changelog | 131 +++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 58fa5cf91..2eabc0430 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,131 @@ +2014-03-01 v0.1.22 +================== + + * 2014-03-01 Matías Aguirre + v0.1.22 + + * 2014-02-28 Andrey Kuzmin + Fixes broken email confirmation for SQLAlchemy storage and webpy_app + + * 2014-02-27 Sebastian Bassi + Update mendeley.py + + * 2014-02-27 Matías Aguirre + Don't update user if it's set to None (non-authenticated pipeline + continuation). Refs #198 + + * 2014-02-27 Matías Aguirre + Set is_new flag on pipeline if user is not new. Refs #201 + + * 2014-02-27 Matías Aguirre + Better error message + + * 2014-02-25 Matías Aguirre + Add 'user' to default scope on coinbase backend. Closes #199 + + * 2014-02-24 Matías Aguirre + User USERNAME_FIELD on mongoengine. Closes #197 + + * 2014-02-21 Matías Aguirre + Dev marker + + * 2014-02-18 David Kingman + Removed commit marker + + * 2014-02-13 Matías Aguirre + PEP8, Python3 and example fixes + + * 2014-02-13 Thomas Lovett + fix copy-paste typo in callback url + + * 2014-02-13 Thomas Lovett + add clef to main README + + * 2014-02-14 Yan Kalchevskiy + Fixed a typo. + + * 2014-02-13 Matías Aguirre + Vimeo backend + + * 2014-02-13 Matías Aguirre + Move badge to the top + + * 2014-02-13 Bitdeli Chef + Add a Bitdeli badge to README + + * 2014-02-13 Matías Aguirre + Docs about associate user by email + + * 2014-02-12 Matías Aguirre + Style recent docs + + * 2014-02-13 Joe B. Lewis + added information for FIELDS_STORED_IN_SESSION + + * 2014-02-12 Hassek + removed extra_data override + + * 2014-02-11 Hassek + updated live connection for better support + + * 2014-02-10 Matías Aguirre + CherryPy mention in project index + + * 2014-02-10 Matías Aguirre + CherryPy docs + + * 2014-02-10 Matías Aguirre + Disconnection on example app + + * 2014-02-10 Matías Aguirre + Pass user on disconnect + + * 2014-02-10 Matías Aguirre + Get extra_data from details on openid too + + * 2014-02-10 Matías Aguirre + Mendeley OAuth2 in example app + + * 2014-02-10 Matías Aguirre + Fix Mendeley OAuth2 implementation, use https URLs + + * 2014-02-10 Matías Aguirre + Switch parent class to avoid overrides + + * 2014-02-10 Matías Aguirre + Finishe CherryPy app support and add example application + + * 2014-02-10 Matías Aguirre + Mendeley OAuth2 docs and thanks to Sebastian Bassi (initial author) + + * 2014-02-10 Matías Aguirre + Mendeley OAuth2 backend + + * 2014-02-10 Matías Aguirre + Fix AuthFailed calls + + * 2014-02-10 Matías Aguirre + Raise social-auth exception on connection error. Closes #155 + + * 2014-02-10 Matías Aguirre + Parse token if it's an string (keep a compatible API). Refs #180 + + * 2014-02-09 Matías Aguirre + Stick with sure 1.2.3 (higher is broken, I should drop sure) + + * 2014-02-09 Matías Aguirre + Update sure to 1.2.5 + + * 2014-02-09 Matías Aguirre + Fix LinkedIn OAuth2 backend, pass access token parameter in querystring. + Closes #181 + 2014-02-05 v0.1.21 ================== + * 2014-02-05 Matías Aguirre + v0.1.21 + * 2014-02-05 Matías Aguirre Fix iexact field lookup. Refs #179 @@ -343,6 +468,9 @@ * 2013-11-21 josseph Update weibo.py + * 2013-11-20 Jesse Pollak + adds clef as a login provider + * 2013-11-20 maxtepkeev Make vk-app backend to retrieve additional user data in respect to the *_EXTRA_DATA setting @@ -1545,6 +1673,9 @@ * 2013-03-29 Matías Aguirre Remove stop-pipeline exception (not used at all) + * 2013-03-29 Matías Aguirre + Initial cherrypy support + * 2013-03-26 Matías Aguirre Fix exception raised on skyrock backend diff --git a/social/__init__.py b/social/__init__.py index e993f2fda..d95a07f6d 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 1, 22) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 3c223e4108aebe4986b9389b11b7571c9e899f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Mar 2014 04:47:39 -0200 Subject: [PATCH 151/890] Mark dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index d95a07f6d..e462610ee 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 22) -extra = '' +version = (0, 1, 23) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 1573884f61ced68c398ce1768c7013ebf12d0c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Mar 2014 16:31:21 -0200 Subject: [PATCH 152/890] Docs about flask error handling --- docs/configuration/flask.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/configuration/flask.rst b/docs/configuration/flask.rst index c47e92ade..044859882 100644 --- a/docs/configuration/flask.rst +++ b/docs/configuration/flask.rst @@ -91,8 +91,33 @@ handlers to these:: return {'user': None} +Exceptions handling +------------------- + +The Django application has a middleware (that fits in the framework +architecture) to facilitate the different exceptions_ handling raised by +python-social-auth_. The same can be accomplished (even on a simpler way) in +Flask by defining an errorhandler_. For example the next code will redirect any +social-auth exception to a ``/socialerror`` URL:: + + from social.exceptions import SocialAuthBaseException + + + @app.errorhandler(500) + def error_handler(error): + if isinstance(error, SocialAuthBaseException): + return redirect('/socialerror') + + +Be sure to set your debug and test flags to ``False`` when testing this on your +development environment, otherwise the exception will be raised and error +handlers won't be called. + + .. _Flask Blueprint: http://flask.pocoo.org/docs/blueprints/ .. _Flask-Login: https://github.com/maxcountryman/flask-login .. _python-social-auth: https://github.com/omab/python-social-auth .. _Flask built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/flask_app .. _sqlalchemy: http://www.sqlalchemy.org/ +.. _exceptions: https://github.com/omab/python-social-auth/blob/master/social/exceptions.py +.. _errorhandler: http://flask.pocoo.org/docs/api/#flask.Flask.errorhandler From 44b9ef1dbc62c2937a439fed7e4500acea1de0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Mar 2014 16:33:17 -0200 Subject: [PATCH 153/890] Link backend docs in index --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 46da01a14..b7371c4ac 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -54,6 +54,7 @@ Social backends belgium_eid bitbucket box + clef coinbase dailymotion disqus From ec562c44bd87a22b9bb3b20d44a1938d6c7e9aad Mon Sep 17 00:00:00 2001 From: Peter Schmidt Date: Thu, 6 Mar 2014 11:53:25 +1100 Subject: [PATCH 154/890] Add some missing dependencies for running `social.apps.django_app.default.tests` --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 93793e5e8..2b5aabb6e 100644 --- a/setup.py +++ b/setup.py @@ -75,5 +75,6 @@ def get_packages(): 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3'], + tests_require=['sure>=1.2.5', 'httpretty>=0.8.0', 'mock>=1.0.1'], test_suite='social.tests', zip_safe=False) From 11ef865c8b0b38edee72a975d05cda925535f60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 6 Mar 2014 23:06:07 -0200 Subject: [PATCH 155/890] Remove bitdeli badge --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index 0ac8ef834..b35775001 100644 --- a/README.rst +++ b/README.rst @@ -17,10 +17,6 @@ for more frameworks and ORMs. .. image:: https://pypip.in/d/python-social-auth/badge.png :target: https://crate.io/packages/python-social-auth?version=latest -.. image:: https://d2weczhvl823v0.cloudfront.net/omab/python-social-auth/trend.png - :alt: Bitdeli badge - :target: https://bitdeli.com/free - .. contents:: Table of Contents From f7527f4cfed2ccff866150b07a374ec7973bdab8 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 8 Mar 2014 19:40:19 +0100 Subject: [PATCH 156/890] Fixed Django < 1.4 support in context processors. Prior to Django commit 60cf3f2f842b6e56132b8880c70acc87bd753c, Django was using None as a sentinel value. --- social/apps/django_app/context_processors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/context_processors.py b/social/apps/django_app/context_processors.py index 5b0080f33..fb70e60ce 100644 --- a/social/apps/django_app/context_processors.py +++ b/social/apps/django_app/context_processors.py @@ -5,7 +5,7 @@ from django.utils.functional import empty as _empty empty = _empty except ImportError: # django < 1.4 - empty = object() + empty = None from social.backends.utils import user_backends_data From babfd43205cc77d34211b5bd547eef694192382d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 9 Mar 2014 03:21:07 -0300 Subject: [PATCH 157/890] Make oauth_token retrieval optional. Refs #212 --- social/backends/facebook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index ee4804461..c823c2624 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -129,7 +129,7 @@ def auth_complete(self, *args, **kwargs): if response is not None: access_token = response.get('access_token') or \ - response['oauth_token'] or \ + response.get('oauth_token') or \ self.data.get('access_token') if access_token is None: From 582ed5fc1b3c71bd46314d166e2f423ece4c5306 Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Wed, 12 Mar 2014 17:42:06 +0000 Subject: [PATCH 158/890] Added backend for Ubuntu (One). --- social/backends/ubuntu.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 social/backends/ubuntu.py diff --git a/social/backends/ubuntu.py b/social/backends/ubuntu.py new file mode 100644 index 000000000..64819c1de --- /dev/null +++ b/social/backends/ubuntu.py @@ -0,0 +1,16 @@ +""" +Ubuntu One OpenId backend +""" +from social.backends.open_id import OpenIdAuth + + +class UbuntuOpenId(OpenIdAuth): + name = 'ubuntu' + URL = 'https://login.ubuntu.com' + + def get_user_id(self, details, response): + """ + Return user unique id provided by service. For Ubuntu One + the nickname should be original. + """ + return details['nickname'] From 69cc2eead7e958bb9dead8b7b39521538cd2636c Mon Sep 17 00:00:00 2001 From: Andrey Kuzmin Date: Thu, 13 Mar 2014 15:18:45 +0400 Subject: [PATCH 159/890] Removes flask dependency from webpy_app --- social/apps/webpy_app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/webpy_app/__init__.py b/social/apps/webpy_app/__init__.py index e98cdc1cc..811d0f251 100644 --- a/social/apps/webpy_app/__init__.py +++ b/social/apps/webpy_app/__init__.py @@ -1,5 +1,5 @@ from social.strategies.utils import set_current_strategy_getter -from social.apps.flask_app.utils import load_strategy +from social.apps.webpy_app.utils import load_strategy set_current_strategy_getter(load_strategy) From bf6edaa801d9b3a8b83c450e63fddad93073fc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Mar 2014 12:13:29 -0300 Subject: [PATCH 160/890] Mention localhost limitation on facebook. Closes #207 --- docs/backends/facebook.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/backends/facebook.rst b/docs/backends/facebook.rst index eba8c2fa5..04991f372 100644 --- a/docs/backends/facebook.rst +++ b/docs/backends/facebook.rst @@ -7,7 +7,10 @@ OAuth2 Facebook uses OAuth2 for its auth process. Further documentation at `Facebook development resources`_: -- Register a new application at `Facebook App Creation`_, and +- Register a new application at `Facebook App Creation`_, don't use + ``localhost`` as ``App Domains`` and ``Site URL`` since Facebook won't allow + them. Use a placeholder like ``myapp.com`` and define that domain in your + ``/etc/hosts`` or similar file. - fill ``App Id`` and ``App Secret`` values in values:: From 37068c9819b9deaf43ac1053b4fa7e5f63a40c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Mar 2014 13:53:35 -0300 Subject: [PATCH 161/890] Use forms to disconnect --- examples/cherrypy_example/templates/base.html | 11 ----------- .../django_example/example/templates/base.html | 11 ----------- .../django_example/example/templates/done.html | 17 +++++++++-------- .../example/templates/base.html | 11 ----------- .../example/templates/done.html | 15 ++++++++------- examples/flask_example/templates/base.html | 11 ----------- examples/flask_example/templates/done.html | 15 ++++++++------- examples/tornado_example/templates/base.html | 11 ----------- examples/webpy_example/templates/base.html | 11 ----------- examples/webpy_example/templates/done.html | 16 +++++++++------- 10 files changed, 34 insertions(+), 95 deletions(-) diff --git a/examples/cherrypy_example/templates/base.html b/examples/cherrypy_example/templates/base.html index db47aa234..86db50440 100644 --- a/examples/cherrypy_example/templates/base.html +++ b/examples/cherrypy_example/templates/base.html @@ -8,18 +8,7 @@ {% block content %}{% endblock %} {% block scripts %}{% endblock %} -
- diff --git a/examples/django_example/example/templates/base.html b/examples/django_example/example/templates/base.html index b60e1c40f..ef432efdc 100644 --- a/examples/django_example/example/templates/base.html +++ b/examples/django_example/example/templates/base.html @@ -9,18 +9,7 @@ {% block content %}{% endblock %} {% block scripts %}{% endblock %} -
{% csrf_token %}
- diff --git a/examples/django_example/example/templates/done.html b/examples/django_example/example/templates/done.html index dfce6fbdb..42f793bc7 100644 --- a/examples/django_example/example/templates/done.html +++ b/examples/django_example/example/templates/done.html @@ -2,16 +2,17 @@ {% load url from future %} {% block content %} -

You are logged in as {{ user.username }}!

+

You are logged in as {{ user.username }}! (Logout)

Associated:

-
    - {% for assoc in backends.associated %} -
  • - {{ assoc.provider }} (Disconnect or logout) -
  • - {% endfor %} -
+{% for assoc in backends.associated %} +
+ {{ assoc.provider }} +
{% csrf_token %} + +
+
+{% endfor %}

Associate:

    diff --git a/examples/django_me_example/example/templates/base.html b/examples/django_me_example/example/templates/base.html index 408c525ba..86db50440 100644 --- a/examples/django_me_example/example/templates/base.html +++ b/examples/django_me_example/example/templates/base.html @@ -8,18 +8,7 @@ {% block content %}{% endblock %} {% block scripts %}{% endblock %} -
    {% csrf_token %}
    - diff --git a/examples/django_me_example/example/templates/done.html b/examples/django_me_example/example/templates/done.html index c881273ac..b24808267 100644 --- a/examples/django_me_example/example/templates/done.html +++ b/examples/django_me_example/example/templates/done.html @@ -5,13 +5,14 @@

    You are logged in as {{ user.username }}!

    Associated:

    -
      - {% for assoc in backends.associated %} -
    • - {{ assoc.provider }} (Disconnect) -
    • - {% endfor %} -
    +{% for assoc in backends.associated %} +
    + {{ assoc.provider }} +
    {% csrf_token %} + +
    +
    +{% endfor %}

    Associate:

      diff --git a/examples/flask_example/templates/base.html b/examples/flask_example/templates/base.html index db47aa234..86db50440 100644 --- a/examples/flask_example/templates/base.html +++ b/examples/flask_example/templates/base.html @@ -8,18 +8,7 @@ {% block content %}{% endblock %} {% block scripts %}{% endblock %} -
      - diff --git a/examples/flask_example/templates/done.html b/examples/flask_example/templates/done.html index d34e35c8f..ccabf53ec 100644 --- a/examples/flask_example/templates/done.html +++ b/examples/flask_example/templates/done.html @@ -4,13 +4,14 @@

      You are logged in as {{ user.username }}!

      Associated:

      -
        - {% for assoc in backends.associated %} -
      • - {{ assoc.provider }} (Disconnect) -
      • - {% endfor %} -
      +{% for assoc in backends.associated %} +
      + {{ assoc.provider }} +
      + +
      +
      +{% endfor %}

      Associate:

        diff --git a/examples/tornado_example/templates/base.html b/examples/tornado_example/templates/base.html index 6c918e8f7..a4e3df313 100644 --- a/examples/tornado_example/templates/base.html +++ b/examples/tornado_example/templates/base.html @@ -8,18 +8,7 @@ {% block content %}{% end %} {% block scripts %}{% end %} -
        - diff --git a/examples/webpy_example/templates/base.html b/examples/webpy_example/templates/base.html index db47aa234..86db50440 100644 --- a/examples/webpy_example/templates/base.html +++ b/examples/webpy_example/templates/base.html @@ -8,18 +8,7 @@ {% block content %}{% endblock %} {% block scripts %}{% endblock %} -
        - diff --git a/examples/webpy_example/templates/done.html b/examples/webpy_example/templates/done.html index 4550eef6b..0ee48cff5 100644 --- a/examples/webpy_example/templates/done.html +++ b/examples/webpy_example/templates/done.html @@ -4,13 +4,15 @@ Logged in as {{ user.username }}!

        Associated:

        -
          - {% for assoc in backends["associated"] %} -
        • - {{ assoc.provider }} (Disconnect) -
        • - {% endfor %} -
        + +{% for assoc in backends["associated"] %} +
        + {{ assoc.provider }} +
        + +
        +
        +{% endfor %}

        Associate:

          From 115873779ee913c9b767f42cef9df2541fa63572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Mar 2014 14:34:07 -0300 Subject: [PATCH 162/890] Simplify redirect cleaner method. Closes #191 --- social/utils.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/social/utils.py b/social/utils.py index 37415aa99..a87d024d3 100644 --- a/social/utils.py +++ b/social/utils.py @@ -48,18 +48,15 @@ def sanitize_redirect(host, redirect_to): and returns it, else returns None, similar as how's it done on django.contrib.auth.views. """ - # Quick sanity check. - if not redirect_to or \ - not isinstance(redirect_to, six.string_types) or \ - getattr(redirect_to, 'decode', None) and \ - not isinstance(redirect_to.decode(), six.string_types): - return None - - # Heavier security check, don't allow redirection to a different host. - netloc = urlparse(redirect_to)[1] - if netloc and netloc != host: - return None - return redirect_to + if redirect_to: + try: + # Don't redirect to a different host + netloc = urlparse(redirect_to)[1] or host + except (TypeError, AttributeError): + pass + else: + if netloc == host: + return redirect_to def user_is_authenticated(user): From dec6d30ab758280cec6562c3c6aa9edc975bf56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Mar 2014 14:39:11 -0300 Subject: [PATCH 163/890] Get social_user instance before login. Refs #190 --- social/apps/django_app/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 20af4e935..ab368cfb6 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -32,10 +32,10 @@ def disconnect(request, backend, association_id=None): def _do_login(strategy, user): - login(strategy.request, user) # user.social_user is the used UserSocialAuth instance defined in # authenticate process social_user = user.social_user + login(strategy.request, user) if strategy.setting('SESSION_EXPIRATION', True): # Set session expiration date if present and not disabled # by setting. Use last social-auth instance for current From d975841343e7e37312267ef5ae42f5f8f20ccac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Mar 2014 15:35:44 -0300 Subject: [PATCH 164/890] Use stateless mode with Steam. Fixes #200 --- social/backends/open_id.py | 8 ++++---- social/backends/steam.py | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index b00797675..11e333315 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -207,12 +207,12 @@ def setup_request(self, params=None): def consumer(self): """Create an OpenID Consumer object for the given Django request.""" if not hasattr(self, '_consumer'): - self._consumer = Consumer( - self.strategy.openid_session_dict(SESSION_NAME), - self.strategy.openid_store() - ) + self._consumer = self.create_consumer(self.strategy.openid_store()) return self._consumer + def create_consumer(self, store=None): + return Consumer(self.strategy.openid_session_dict(SESSION_NAME), store) + def uses_redirect(self): """Return true if openid request will be handled with redirect or HTML content will be returned. diff --git a/social/backends/steam.py b/social/backends/steam.py index 8ae43b9bc..e283a5b63 100644 --- a/social/backends/steam.py +++ b/social/backends/steam.py @@ -34,6 +34,12 @@ def get_user_details(self, response): details = {} return details + def consumer(self): + # Steam seems to support stateless mode only, ignore store + if not hasattr(self, '_consumer'): + self._consumer = self.create_consumer() + return self._consumer + def _user_id(self, response): user_id = response.identity_url.rsplit('/', 1)[-1] if not user_id.isdigit(): From ba2a926c3eeeab6279ab3188faadac326afff780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Mar 2014 16:17:13 -0300 Subject: [PATCH 165/890] Remove symlinks. Fixes #177 --- social/apps/django_app/default/tests.py | 2 +- social/apps/django_app/me/tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) mode change 120000 => 100644 social/apps/django_app/default/tests.py mode change 120000 => 100644 social/apps/django_app/me/tests.py diff --git a/social/apps/django_app/default/tests.py b/social/apps/django_app/default/tests.py deleted file mode 120000 index 84bdbb213..000000000 --- a/social/apps/django_app/default/tests.py +++ /dev/null @@ -1 +0,0 @@ -../tests.py \ No newline at end of file diff --git a/social/apps/django_app/default/tests.py b/social/apps/django_app/default/tests.py new file mode 100644 index 000000000..db1bc1d7c --- /dev/null +++ b/social/apps/django_app/default/tests.py @@ -0,0 +1 @@ +from social.apps.django_app.tests import * diff --git a/social/apps/django_app/me/tests.py b/social/apps/django_app/me/tests.py deleted file mode 120000 index 84bdbb213..000000000 --- a/social/apps/django_app/me/tests.py +++ /dev/null @@ -1 +0,0 @@ -../tests.py \ No newline at end of file diff --git a/social/apps/django_app/me/tests.py b/social/apps/django_app/me/tests.py new file mode 100644 index 000000000..db1bc1d7c --- /dev/null +++ b/social/apps/django_app/me/tests.py @@ -0,0 +1 @@ +from social.apps.django_app.tests import * From 7ff6ebf94973f3a12ce46ebf4eb409d67f17ca9e Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Sat, 15 Mar 2014 17:07:47 -0400 Subject: [PATCH 166/890] added strava support! --- docs/backends/strava.rst | 17 ++++++++ social/backends/strava.py | 30 ++++++++++++++ social/tests/backends/test_strava.py | 62 ++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 docs/backends/strava.rst create mode 100644 social/backends/strava.py create mode 100644 social/tests/backends/test_strava.py diff --git a/docs/backends/strava.rst b/docs/backends/strava.rst new file mode 100644 index 000000000..3623c830e --- /dev/null +++ b/docs/backends/strava.rst @@ -0,0 +1,17 @@ +Strava +========= + +Strava uses OAuth v2 for Authentication. + +- Register a new application at the `Strava API`_, and + +- fill ``Client ID`` and ``Client Secret`` from strava.com values in the settings:: + + SOCIAL_AUTH_STRAVA_KEY = '' + SOCIAL_AUTH_STRAVA_SECRET = '' + +- extra scopes can be defined by using:: + + SOCIAL_AUTH_INSTAGRAM_AUTH_EXTRA_ARGUMENTS = {'scope': 'likes comments relationships'} + +.. _Strava API: https://www.strava.com/settings/api diff --git a/social/backends/strava.py b/social/backends/strava.py new file mode 100644 index 000000000..7da2fad3c --- /dev/null +++ b/social/backends/strava.py @@ -0,0 +1,30 @@ +""" +Strava OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/strava.html +""" +from social.backends.oauth import BaseOAuth2 + + +class StravaOAuth(BaseOAuth2): + name = 'strava' + AUTHORIZATION_URL = 'https://www.strava.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://www.strava.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + + def get_user_id(self, details, response): + return response['athlete']['id'] + + def get_user_details(self, response): + """Return user details from Strava account""" + username = response['athlete']['id'] # because there is no usernames on strava + fullname = response['athlete'].get('first_name', '') + fullname += ' %s' % response['athlete'].get('lasst_name', '') + email = response['athlete'].get('email', '') + return {'username': username, + 'first_name': fullname, + 'email': email} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://www.strava.com/api/v3/athlete', + params={'access_token': access_token}) \ No newline at end of file diff --git a/social/tests/backends/test_strava.py b/social/tests/backends/test_strava.py new file mode 100644 index 000000000..28578a239 --- /dev/null +++ b/social/tests/backends/test_strava.py @@ -0,0 +1,62 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class InstagramOAuth2Test(OAuth2Test): + backend_path = 'social.backends.strava.StravaOAuth' + user_data_url = 'https://www.strava.com/api/v3/athlete' + expected_username = 227615 + access_token_body = json.dumps({ + "access_token": "83ebeabdec09f6670863766f792ead24d61fe3f9", + "athlete": { + "id": 227615, + "resource_state": 3, + "firstname": "John", + "lastname": "Applestrava", + "profile_medium": "http://pics.com/227615/medium.jpg", + "profile": "http://pics.com/227615/large.jpg", + "city": "San Francisco", + "state": "California", + "country": "United States", + "sex": "M", + "friend": null, + "follower": null, + "premium": true, + "created_at": "2008-01-01T17:44:00Z", + "updated_at": "2013-09-04T20:00:50Z", + "follower_count": 273, + "friend_count": 19, + "mutual_friend_count": 0, + "date_preference": "%m/%d/%Y", + "measurement_preference": "feet", + "email": "john@applestrava.com", + "clubs": [ ], + "bikes": [ ], + "shoes": [ ] + } + }) + user_data_body = json.dumps({ + "id": 227615, + "resource_state": 2, + "firstname": "John", + "lastname": "Applestrava", + "profile_medium": "http://pics.com/227615/medium.jpg", + "profile": "http://pics.com/227615/large.jpg", + "city": "San Francisco", + "state": "CA", + "country": "United States", + "sex": "M", + "friend": null, + "follower": "accepted", + "premium": true, + "created_at": "2011-03-19T21:59:57Z", + "updated_at": "2013-09-05T16:46:54Z", + "approve_followers": false + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 3e097668ccdee22536777b1507c03dc95a61f19a Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Sat, 15 Mar 2014 17:11:33 -0400 Subject: [PATCH 167/890] updated some docs --- README.rst | 2 ++ examples/django_me_example/example/templates/home.html | 1 + 2 files changed, 3 insertions(+) diff --git a/README.rst b/README.rst index b35775001..35a5106b6 100644 --- a/README.rst +++ b/README.rst @@ -97,6 +97,7 @@ or current ones extended): * Stackoverflow_ OAuth2 * Steam_ OpenId * Stocktwits_ OAuth2 + * Strava_ OAuth2 * Stripe_ OAuth2 * Taobao_ OAuth2 http://open.taobao.com/doc/detail.htm?id=118 * ThisIsMyJam_ OAuth1 https://www.thisismyjam.com/developers/authentication @@ -239,6 +240,7 @@ check `django-social-auth LICENSE`_ for details: .. _Skyrock: https://skyrock.com .. _Soundcloud: https://soundcloud.com .. _Stocktwits: https://stocktwits.com +.. _Strava: http://strava.com .. _Stripe: https://stripe.com .. _Taobao: http://open.taobao.com/doc/detail.htm?id=118 .. _Tripit: https://www.tripit.com diff --git a/examples/django_me_example/example/templates/home.html b/examples/django_me_example/example/templates/home.html index 7c11a3a28..066a44307 100644 --- a/examples/django_me_example/example/templates/home.html +++ b/examples/django_me_example/example/templates/home.html @@ -9,6 +9,7 @@ Yahoo OpenId
          Yahoo OAuth
          Stripe OAuth2
          +Strava OAuth2
          Facebook OAuth2
          Facebook App
          Angel OAuth2
          From a8407fa8750f3e8b3f9cb4b4f0c9cb4f2a1228a3 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Sat, 15 Mar 2014 17:51:07 -0400 Subject: [PATCH 168/890] final changes --- social/backends/strava.py | 5 ++--- social/tests/backends/test_strava.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/social/backends/strava.py b/social/backends/strava.py index 7da2fad3c..036375aef 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -17,11 +17,10 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Strava account""" username = response['athlete']['id'] # because there is no usernames on strava - fullname = response['athlete'].get('first_name', '') - fullname += ' %s' % response['athlete'].get('lasst_name', '') + first_name = response['athlete'].get('first_name', '') email = response['athlete'].get('email', '') return {'username': username, - 'first_name': fullname, + 'first_name': first_name, 'email': email} def user_data(self, access_token, *args, **kwargs): diff --git a/social/tests/backends/test_strava.py b/social/tests/backends/test_strava.py index 28578a239..286d8c371 100644 --- a/social/tests/backends/test_strava.py +++ b/social/tests/backends/test_strava.py @@ -3,7 +3,7 @@ from social.tests.backends.oauth import OAuth2Test -class InstagramOAuth2Test(OAuth2Test): +class StravaOAuthTest(OAuth2Test): backend_path = 'social.backends.strava.StravaOAuth' user_data_url = 'https://www.strava.com/api/v3/athlete' expected_username = 227615 From 97993e79a0f15bf35b34dae7c38e973a6b8ddb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 16 Mar 2014 15:47:09 -0300 Subject: [PATCH 169/890] Fix strava tests and username generation. Refs #217 --- social/backends/strava.py | 7 ++++--- social/tests/backends/test_strava.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/social/backends/strava.py b/social/backends/strava.py index 036375aef..e80c8a632 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -16,14 +16,15 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Strava account""" - username = response['athlete']['id'] # because there is no usernames on strava + # because there is no usernames on strava + username = response['athlete']['id'] first_name = response['athlete'].get('first_name', '') email = response['athlete'].get('email', '') - return {'username': username, + return {'username': str(username), 'first_name': first_name, 'email': email} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://www.strava.com/api/v3/athlete', - params={'access_token': access_token}) \ No newline at end of file + params={'access_token': access_token}) diff --git a/social/tests/backends/test_strava.py b/social/tests/backends/test_strava.py index 286d8c371..ae088b4c6 100644 --- a/social/tests/backends/test_strava.py +++ b/social/tests/backends/test_strava.py @@ -6,7 +6,7 @@ class StravaOAuthTest(OAuth2Test): backend_path = 'social.backends.strava.StravaOAuth' user_data_url = 'https://www.strava.com/api/v3/athlete' - expected_username = 227615 + expected_username = '227615' access_token_body = json.dumps({ "access_token": "83ebeabdec09f6670863766f792ead24d61fe3f9", "athlete": { @@ -20,9 +20,9 @@ class StravaOAuthTest(OAuth2Test): "state": "California", "country": "United States", "sex": "M", - "friend": null, - "follower": null, - "premium": true, + "friend": "null", + "follower": "null", + "premium": "true", "created_at": "2008-01-01T17:44:00Z", "updated_at": "2013-09-04T20:00:50Z", "follower_count": 273, @@ -31,9 +31,9 @@ class StravaOAuthTest(OAuth2Test): "date_preference": "%m/%d/%Y", "measurement_preference": "feet", "email": "john@applestrava.com", - "clubs": [ ], - "bikes": [ ], - "shoes": [ ] + "clubs": [], + "bikes": [], + "shoes": [] } }) user_data_body = json.dumps({ @@ -47,12 +47,12 @@ class StravaOAuthTest(OAuth2Test): "state": "CA", "country": "United States", "sex": "M", - "friend": null, + "friend": "null", "follower": "accepted", - "premium": true, + "premium": "true", "created_at": "2011-03-19T21:59:57Z", "updated_at": "2013-09-05T16:46:54Z", - "approve_followers": false + "approve_followers": "false" }) def test_login(self): From 28760c083e2a5acda6fc2511302159bdfa897f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 18 Mar 2014 11:03:02 -0300 Subject: [PATCH 170/890] Register by token use case --- docs/use_cases.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 463e075ca..4f5f4ca20 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -103,5 +103,44 @@ doesn't validate his email with that same account. Finally this user will turn to your site (which supports that provider) and sign up to it, since the email is the same, the malicious user will take control over the User A account. + +Signup by OAuth access_token +---------------------------- + +It's a common scenario that mobile applications will use an SDK to signup +a user withing the app, but that signup won't be reflected by +``python-socia-auth`` unless the corresponding database entries are created. In +order to do so, it's possible to create a view / route that creates those +entries by a given ``access_token``. Take the following code for instance (the +code follows Django conventions, but versions for others frameworks can be +implemented easily):: + + from django.contrib.auth import login + from social.apps.django_app.utils import strategy + + # Define an URL entry to point to this view, call it passing the + # access_token parameter like ?access_token=. The URL entry must + # contain the backend, like this: + # + # url(r'^register-by-token/(?P[^/]+)/$', + # 'register_by_access_token') + + @strategy('social:complete') + def register_by_access_token(request, backend): + # This view expects an access_token GET parameter + token = request.GET.get('access_token') + user = backend.do_auth(request.GET.get('access_token')) + if user: + login(request, user) + return 'OK' + else: + return 'ERROR' + +The snipped above is quite simple, it doesn't return JSON and usually this call +will be done by AJAX. It doesn't return the user information, but that's +something that can be extended and filled to suit the project where it's going +to be used. + + .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list From f8fa68e7a6dc4a9f82ec1236ee82fd35aa2ef8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 18 Mar 2014 11:35:04 -0300 Subject: [PATCH 171/890] Multiple scopes use case --- docs/use_cases.rst | 64 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 4f5f4ca20..a2eee8d3f 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -109,7 +109,7 @@ Signup by OAuth access_token It's a common scenario that mobile applications will use an SDK to signup a user withing the app, but that signup won't be reflected by -``python-socia-auth`` unless the corresponding database entries are created. In +python-socia-auth_ unless the corresponding database entries are created. In order to do so, it's possible to create a view / route that creates those entries by a given ``access_token``. Take the following code for instance (the code follows Django conventions, but versions for others frameworks can be @@ -142,5 +142,67 @@ something that can be extended and filled to suit the project where it's going to be used. +Multiple scopes per provider +---------------------------- + +At the moment python-social-auth_ doesn't provide a method to define multiple +scopes for single backend, this is usually desired since it's recommended to +ask the user for the minimum scope possible and increase the access when it's +really needed. It's possible to add a new backend extending the original one to +accomplish that behavior, there are two ways to do it. + +1. Overriding ``get_scope()`` method:: + + from social.backends.facebook import FacebookOAuth2 + + + class CustomFacebookOAuth2(FacebookOauth2): + def get_scope(self): + scope = super(CustomFacebookOAuth2, self).get_scope() + if self.data.get('extrascope'): + scope += [('foo', 'bar')] + return scope + + + This method is quite simple, it overrides the method that returns the scope + value in a backend (``get_scope()``) and adds extra values tot he list if it + was indicated by a parameter in the ``GET`` or ``POST`` data + (``self.data``). + + Put this new backend in some place in your project and replace the original + ``FacebookOAuth2`` in ``AUTHENTICATION_BACKENDS`` with this new version. + +2. It's possible to do the same by defining a second backend which extends from + the original but overrides the name, this will imply new URLs and also new + settings for the new backend (since the name is used to build the settings + names), it also implies a new application in the provider since not all + providers give you the option of defining multiple redirect URLs. To do it + just add a backend like:: + + from social.backends.facebook import FacebookOAuth2 + + + class CustomFacebookOAuth2(FacebookOauth2): + name = 'facebook-custom' + + Put this new backend in some place in your project keeping the original + ``FacebookOAuth2`` in ``AUTHENTICATION_BACKENDS``. Now a new set of URLs + will be functional:: + + /login/facebook-custom + /complete/facebook-custom + /disconnect/facebook-custom + + And also a new set of settings:: + + SOCIAL_AUTH_FACEBOOK_CUSTOM_KEY = '...' + SOCIAL_AUTH_FACEBOOK_CUSTOM_SECRET = '...' + SOCIAL_AUTH_FACEBOOK_CUSTOM_SCOPE = [...] + + When the extra permissions are needed, just redirect the user to + ``/login/facebook-custom`` and then get the social auth entry for this new + backend with ``user.social_auth.get(provider='facebook-custom')`` and use + the ``access_token`` in it. + .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list From 27cb1118816df159276adb08ccd04238e9ad67ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 23 Mar 2014 12:40:58 -0300 Subject: [PATCH 172/890] Pass the social_user to login functions. Refs #190 --- social/actions.py | 2 +- social/apps/cherrypy_app/views.py | 2 +- social/apps/django_app/views.py | 5 +---- social/apps/flask_app/routes.py | 2 +- social/apps/tornado_app/handlers.py | 8 +++++--- social/apps/webpy_app/app.py | 8 +++++--- social/tests/actions/actions.py | 6 +++--- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/social/actions.py b/social/actions.py index ca5694d3d..90b7cc36e 100644 --- a/social/actions.py +++ b/social/actions.py @@ -58,7 +58,7 @@ def do_complete(strategy, login, user=None, redirect_name='next', # catch is_new/social_user in case login() resets the instance is_new = getattr(user, 'is_new', False) social_user = user.social_user - login(strategy, user) + login(strategy, user, social_user) # store last login backend name in session strategy.session_set('social_auth_last_login_backend', social_user.provider) diff --git a/social/apps/cherrypy_app/views.py b/social/apps/cherrypy_app/views.py index a9a4cd23b..899384e1a 100644 --- a/social/apps/cherrypy_app/views.py +++ b/social/apps/cherrypy_app/views.py @@ -24,5 +24,5 @@ def disconnect(self, backend, association_id=None): user = getattr(cherrypy.request, 'user', None) return do_disconnect(self.strategy, user, association_id) - def do_login(self, strategy, user): + def do_login(self, strategy, user, social_user): strategy.session_set('user_id', user.id) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index ab368cfb6..585811459 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -31,10 +31,7 @@ def disconnect(request, backend, association_id=None): redirect_name=REDIRECT_FIELD_NAME) -def _do_login(strategy, user): - # user.social_user is the used UserSocialAuth instance defined in - # authenticate process - social_user = user.social_user +def _do_login(strategy, user, social_user): login(strategy.request, user) if strategy.setting('SESSION_EXPIRATION', True): # Set session expiration date if present and not disabled diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index 286402b19..d12e28358 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -33,7 +33,7 @@ def disconnect(backend, association_id=None): return do_disconnect(g.strategy, g.user, association_id) -def do_login(strategy, user): +def do_login(strategy, user, social_user): return login_user(user, remember=request.cookies.get('remember') or request.args.get('remember') or request.form.get('remember') or False) diff --git a/social/apps/tornado_app/handlers.py b/social/apps/tornado_app/handlers.py index 2c8452d18..f6769272b 100644 --- a/social/apps/tornado_app/handlers.py +++ b/social/apps/tornado_app/handlers.py @@ -38,9 +38,11 @@ def post(self, backend): @strategy('complete') def _complete(self, backend): - do_complete(self.strategy, - login=lambda strategy, user: self.login_user(user), - user=self.get_current_user()) + do_complete( + self.strategy, + login=lambda strategy, user, social_user: self.login_user(user), + user=self.get_current_user() + ) class DisconnectHandler(BaseHandler): diff --git a/social/apps/webpy_app/app.py b/social/apps/webpy_app/app.py index d6e6543d3..3135fb2c4 100644 --- a/social/apps/webpy_app/app.py +++ b/social/apps/webpy_app/app.py @@ -55,9 +55,11 @@ def POST(self, backend, *args, **kwargs): @strategy('/complete/%(backend)s/') def _complete(self, backend, *args, **kwargs): - return do_complete(self.strategy, - login=lambda strat, user: self.login_user(user), - user=self.get_current_user(), *args, **kwargs) + return do_complete( + self.strategy, + login=lambda strat, user, social_user: self.login_user(user), + user=self.get_current_user(), *args, **kwargs + ) class disconnect(BaseViewClass): diff --git a/social/tests/actions/actions.py b/social/tests/actions/actions.py index 19b2bdda3..8e1c15604 100644 --- a/social/tests/actions/actions.py +++ b/social/tests/actions/actions.py @@ -120,8 +120,8 @@ def do_login(self, after_complete_checks=True, user_data_body=None, redirect = do_complete( self.strategy, user=self.user, - login=lambda strategy, user: strategy.session_set('username', - user.username) + login=lambda strategy, user, social_user: + strategy.session_set('username', user.username) ) if after_complete_checks: expect(self.strategy.session_get('username')).to.equal( @@ -184,7 +184,7 @@ def do_login_with_partial_pipeline(self, before_complete=None): content_type='text/json') self.strategy.set_request_data(location_query) - def _login(strategy, user): + def _login(strategy, user, social_user): strategy.session_set('username', user.username) redirect = do_complete(self.strategy, user=self.user, login=_login) From 062552926c30379d13058ad11a3c9a76663c9b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 23 Mar 2014 13:06:15 -0300 Subject: [PATCH 173/890] Don't assign strategy in middleware. Closes #221 --- social/apps/django_app/middleware.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index d76e5c26e..f456118fb 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -21,12 +21,12 @@ class SocialAuthExceptionMiddleware(object): get_redirect_uri methods, which each accept request and exception. """ def process_exception(self, request, exception): - self.strategy = getattr(request, 'social_strategy', None) - if self.strategy is None or self.raise_exception(request, exception): + strategy = getattr(request, 'social_strategy', None) + if strategy is None or self.raise_exception(request, exception): return if isinstance(exception, SocialAuthBaseException): - backend_name = self.strategy.backend.name + backend_name = strategy.backend.name message = self.get_message(request, exception) url = self.get_redirect_uri(request, exception) @@ -42,10 +42,13 @@ def process_exception(self, request, exception): return redirect(url) def raise_exception(self, request, exception): - return self.strategy.setting('RAISE_EXCEPTIONS', settings.DEBUG) + strategy = getattr(request, 'social_strategy', None) + if strategy is not None: + return strategy.setting('RAISE_EXCEPTIONS', settings.DEBUG) def get_message(self, request, exception): return six.text_type(exception) def get_redirect_uri(self, request, exception): - return self.strategy.setting('LOGIN_ERROR_URL') + strategy = getattr(request, 'social_strategy', None) + return strategy.setting('LOGIN_ERROR_URL') From 435fec3bad94489cff7c946660692ce0effc4e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 23 Mar 2014 14:38:57 -0300 Subject: [PATCH 174/890] Try to use django messages app, fallback to URL. Fixes #210 --- social/apps/django_app/middleware.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index f456118fb..b5d3cc22a 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib import messages +from django.contrib.messages.api import MessageFailure from django.shortcuts import redirect from django.utils.http import urlquote @@ -29,13 +30,10 @@ def process_exception(self, request, exception): backend_name = strategy.backend.name message = self.get_message(request, exception) url = self.get_redirect_uri(request, exception) - - if request.user.is_authenticated(): - # Ensure that messages are added to authenticated users only, - # otherwise this fails + try: messages.error(request, message, extra_tags='social-auth ' + backend_name) - else: + except MessageFailure: url += ('?' in url and '&' or '?') + \ 'message={0}&backend={1}'.format(urlquote(message), backend_name) From be53de53cceea83584820fb0efdaac5e4b46a247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 23 Mar 2014 14:47:40 -0300 Subject: [PATCH 175/890] Comment about enhanced security flag in Live backend. Refs #218 --- docs/backends/live.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/backends/live.rst b/docs/backends/live.rst index 7eba1624b..0ede6c3af 100644 --- a/docs/backends/live.rst +++ b/docs/backends/live.rst @@ -18,4 +18,7 @@ Live uses OAuth2 for its connect workflow, notice that it isn't OAuth WRAP. Defaults are ``wl.basic`` and ``wl.emails``. Latter one is necessary to retrieve user email. +- Ensure to have a valid ``Redirect URL`` (``http://your-domain/complete/live``) + defined in the application if ``Enhanced security redirection`` is enabled. + .. _Live Connect Developer Center: https://account.live.com/developers/applications/create From 18ec52a1c34e263e4d909fc1ee19500f9adac26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 23 Mar 2014 18:42:01 -0300 Subject: [PATCH 176/890] Define a custom user model --- examples/django_example/example/app/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/django_example/example/app/models.py b/examples/django_example/example/app/models.py index 71a836239..5bcf78eb7 100644 --- a/examples/django_example/example/app/models.py +++ b/examples/django_example/example/app/models.py @@ -1,3 +1,6 @@ -from django.db import models +# Define a custom User class to work with django-social-auth +from django.contrib.auth.models import AbstractUser, UserManager -# Create your models here. + +class CustomUser(AbstractUser): + objects = UserManager() From 22a6f591a0f4cbaf7521337f1a4e42350ea28f07 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 24 Mar 2014 11:26:35 +0100 Subject: [PATCH 177/890] OpenStreetMap: no img element if user has no avatar --- social/backends/openstreetmap.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/social/backends/openstreetmap.py b/social/backends/openstreetmap.py index 46e9cd912..be3bc542a 100644 --- a/social/backends/openstreetmap.py +++ b/social/backends/openstreetmap.py @@ -45,9 +45,13 @@ def user_data(self, access_token, *args, **kwargs): except ValueError: return None user = dom.getElementsByTagName('user')[0] + try: + avatar = dom.getElementsByTagName('img')[0].getAttribute('href') + except IndexError: + avatar = None return { 'id': user.getAttribute('id'), 'username': user.getAttribute('display_name'), 'account_created': user.getAttribute('account_created'), - 'avatar': dom.getElementsByTagName('img')[0].getAttribute('href') + 'avatar': avatar } From 053b039945e73f5420ce7a7a997d420a9fddd1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 26 Mar 2014 12:06:13 -0300 Subject: [PATCH 178/890] v0.1.23 --- Changelog | 87 +++++++++++++++++++++++++++++++++++++++++++++- social/__init__.py | 2 +- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 2eabc0430..5b838b815 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,91 @@ -2014-03-01 v0.1.22 +2014-03-26 v0.1.23 ================== + * 2014-03-26 Matías Aguirre + v0.1.23 + + * 2014-03-24 Yohan Boniface + OpenStreetMap: no img element if user has no avatar + + * 2014-03-23 Matías Aguirre + Define a custom user model + + * 2014-03-23 Matías Aguirre + Comment about enhanced security flag in Live backend. Refs #218 + + * 2014-03-23 Matías Aguirre + Try to use django messages app, fallback to URL. Fixes #210 + + * 2014-03-23 Matías Aguirre + Don't assign strategy in middleware. Closes #221 + + * 2014-03-23 Matías Aguirre + Pass the social_user to login functions. Refs #190 + + * 2014-03-18 Matías Aguirre + Multiple scopes use case + + * 2014-03-18 Matías Aguirre + Register by token use case + + * 2014-03-16 Matías Aguirre + Fix strava tests and username generation. Refs #217 + + * 2014-03-15 Auston Bunsen + final changes + + * 2014-03-15 Auston Bunsen + updated some docs + + * 2014-03-15 Auston Bunsen + added strava support! + + * 2014-03-15 Matías Aguirre + Remove symlinks. Fixes #177 + + * 2014-03-15 Matías Aguirre + Use stateless mode with Steam. Fixes #200 + + * 2014-03-15 Matías Aguirre + Get social_user instance before login. Refs #190 + + * 2014-03-15 Matías Aguirre + Simplify redirect cleaner method. Closes #191 + + * 2014-03-15 Matías Aguirre + Use forms to disconnect + + * 2014-03-15 Matías Aguirre + Mention localhost limitation on facebook. Closes #207 + + * 2014-03-13 Andrey Kuzmin + Removes flask dependency from webpy_app + + * 2014-03-12 Dave Murphy + Added backend for Ubuntu (One). + + * 2014-03-09 Matías Aguirre + Make oauth_token retrieval optional. Refs #212 + + * 2014-03-08 Baptiste Mispelon + Fixed Django < 1.4 support in context processors. + + * 2014-03-06 Matías Aguirre + Remove bitdeli badge + + * 2014-03-06 Peter Schmidt + Add some missing dependencies for running + `social.apps.django_app.default.tests` + + * 2014-03-01 Matías Aguirre + Link backend docs in index + + * 2014-03-01 Matías Aguirre + Docs about flask error handling + + * 2014-03-01 Matías Aguirre + Mark dev version + * 2014-03-01 Matías Aguirre v0.1.22 diff --git a/social/__init__.py b/social/__init__.py index e462610ee..20a711983 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 1, 23) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 435290e867ca50f143c0a81f738074762b1a46f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 26 Mar 2014 12:09:02 -0300 Subject: [PATCH 179/890] Flag dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 20a711983..99962032e 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 23) -extra = '' +version = (0, 1, 24) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 78af32ab9ccf9024dd3b4d2e08e4c6c563404188 Mon Sep 17 00:00:00 2001 From: Fernando Date: Tue, 25 Mar 2014 23:27:23 -0400 Subject: [PATCH 180/890] initial version of docker backend --- docs/backends/docker.rst | 19 +++++++++++++++++++ social/backends/docker.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 docs/backends/docker.rst create mode 100644 social/backends/docker.py diff --git a/docs/backends/docker.rst b/docs/backends/docker.rst new file mode 100644 index 000000000..8e4714354 --- /dev/null +++ b/docs/backends/docker.rst @@ -0,0 +1,19 @@ +Docker +====== + +Docker.io OAuth2 +---------------- + +Docker.io now supports OAuth2 for their API. In order to set it up: + +- Register a new application by following the instructions in their website: `Register Your Application`_ + +- Fill **Consumer Key** and **Consumer Secret** values in settings:: + + SOCIAL_AUTH_DOCKER_KEY = '' + SOCIAL_AUTH_DOCKER_SECRET = '' + +- Add ``'social.backends.docker.DockerOAuth2'`` into your + ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. + +.. _Register Your Application: http://docs.docker.io/en/latest/reference/api/docker_io_oauth_api/#register-your-application diff --git a/social/backends/docker.py b/social/backends/docker.py new file mode 100644 index 000000000..e1d28e88a --- /dev/null +++ b/social/backends/docker.py @@ -0,0 +1,36 @@ +""" +Docker.io OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/docker.html +""" +from social.backends.oauth import BaseOAuth2 + + +class DockerOAuth2(BaseOAuth2): + name = 'docker' + ID_KEY = 'user_id' + AUTHORIZATION_URL = 'https://www.docker.io/api/v1.1/o/authorize/' + ACCESS_TOKEN_URL = 'https://www.docker.io/api/v1.1/o/token/' + REFRESH_TOKEN_URL = 'https://www.docker.io/api/v1.1/o/token/' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('refresh_token', 'refresh_token', True), + ('user_id', 'user_id'), + ('email', 'email'), + ('full_name', 'fullname'), + ('location', 'location'), + ('url', 'url'), + ('company', 'company'), + ('gravatar_email', 'gravatar_email'), + ] + + def get_user_details(self, response): + """Return user details from Docker.io account""" + return {'username': response.get('username'), + 'first_name': response.get('full_name', response.get('username')), + 'email': response.get('email', '')} + + def user_data(self, access_token, *args, **kwargs): + """Grab user profile information from Docker.io.""" + return self.get_json('https://www.docker.io/api/v1.1/users/%s/' % kwargs['response']['username'], + headers={"Authorization": "Bearer %s" % access_token}) \ No newline at end of file From 45fda5fa5cf41a75e93cb4551024631310c528dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Mar 2014 01:46:40 -0300 Subject: [PATCH 181/890] PEP8 --- social/backends/docker.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/social/backends/docker.py b/social/backends/docker.py index e1d28e88a..b91512a7a 100644 --- a/social/backends/docker.py +++ b/social/backends/docker.py @@ -26,11 +26,16 @@ class DockerOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Docker.io account""" - return {'username': response.get('username'), - 'first_name': response.get('full_name', response.get('username')), - 'email': response.get('email', '')} + return { + 'username': response.get('username'), + 'first_name': response.get('full_name', response.get('username')), + 'email': response.get('email', '') + } def user_data(self, access_token, *args, **kwargs): """Grab user profile information from Docker.io.""" - return self.get_json('https://www.docker.io/api/v1.1/users/%s/' % kwargs['response']['username'], - headers={"Authorization": "Bearer %s" % access_token}) \ No newline at end of file + username = kwargs['response']['username'] + return self.get_json( + 'https://www.docker.io/api/v1.1/users/%s/' % username, + headers={'Authorization': 'Bearer %s' % access_token} + ) From 04ed21033f88ae9addeacabb1347f255b63c8fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Mar 2014 01:47:31 -0300 Subject: [PATCH 182/890] Link docker docs in backends index --- docs/backends/docker.rst | 3 ++- docs/backends/index.rst | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/backends/docker.rst b/docs/backends/docker.rst index 8e4714354..7e104d777 100644 --- a/docs/backends/docker.rst +++ b/docs/backends/docker.rst @@ -6,7 +6,8 @@ Docker.io OAuth2 Docker.io now supports OAuth2 for their API. In order to set it up: -- Register a new application by following the instructions in their website: `Register Your Application`_ +- Register a new application by following the instructions in their website: + `Register Your Application`_ - Fill **Consumer Key** and **Consumer Secret** values in settings:: diff --git a/docs/backends/index.rst b/docs/backends/index.rst index b7371c4ac..a5526df5a 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -58,6 +58,7 @@ Social backends coinbase dailymotion disqus + docker douban dropbox evernote From b78030badd7f0bab65c7a88fa61ec31119d40d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Czes=C5=82aw=20Kalmus?= Date: Thu, 27 Mar 2014 11:26:16 +0100 Subject: [PATCH 183/890] login with bitbucket account, error when any verified email is set --- social/backends/bitbucket.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index db14f84c8..a7797c946 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -3,6 +3,7 @@ http://psa.matiasaguirre.net/docs/backends/bitbucket.html """ from social.backends.oauth import BaseOAuth1 +from social.exceptions import AuthForbidden class BitbucketOAuth(BaseOAuth1): @@ -37,11 +38,14 @@ def user_data(self, access_token): # the top email emails = self.get_json('https://bitbucket.org/api/1.0/emails/', auth=self.oauth_auth(access_token)) + email = None for address in reversed(emails): if address['active']: email = address['email'] if address['primary']: break + if email is None: + raise AuthForbidden(self, "Bitbucket account has any verified email") return dict(self.get_json('https://bitbucket.org/api/1.0/users/' + email)['user'], email=email) From b1fa8bca3eb7abce29439809ace6dcbfc5578769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Mar 2014 22:31:59 -0300 Subject: [PATCH 184/890] Improve partial session cleaner code. Refs #231 --- social/strategies/base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/social/strategies/base.py b/social/strategies/base.py index 9a64c106e..383e58b7f 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -33,6 +33,10 @@ class BaseStrategy(object): ALLOWED_CHARS = 'abcdefghijklmnopqrstuvwxyz' \ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '0123456789' + # well-known serializable types + SERIALIZABLE_TYPES = (dict, list, tuple, set, bool, type(None)) + \ + six.integer_types + six.string_types + \ + (six.text_type, six.binary_type,) def __init__(self, backend=None, storage=None, request=None, tpl=BaseTemplateStrategy, backends=None, *args, **kwargs): @@ -129,11 +133,7 @@ def partial_to_session(self, next, backend, request=None, *args, **kwargs): 'uid': social.uid } or None } - # Only allow well-known serializable types - types = (dict, list, tuple, set) + six.integer_types + \ - six.string_types + (six.text_type,) + (six.binary_type,) - clean_kwargs.update((name, value) for name, value in kwargs.items() - if isinstance(value, types)) + clean_kwargs.update(kwargs) # Clean any MergeDict data type from the values clean_kwargs.update((name, dict(value)) for name, value in clean_kwargs.items() @@ -143,7 +143,8 @@ def partial_to_session(self, next, backend, request=None, *args, **kwargs): 'backend': backend.name, 'args': tuple(map(self.to_session_value, args)), 'kwargs': dict((key, self.to_session_value(val)) - for key, val in clean_kwargs.items()) + for key, val in clean_kwargs.items() + if isinstance(val, self.SERIALIZABLE_TYPES)) } def partial_from_session(self, session): From 3e959e9bb2b95f36d0c0163f79742abae460e18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Mar 2014 22:32:16 -0300 Subject: [PATCH 185/890] Avoid passing multiple arguments to disconnect partial pipeline --- social/actions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/social/actions.py b/social/actions.py index 90b7cc36e..376e7e5e2 100644 --- a/social/actions.py +++ b/social/actions.py @@ -92,8 +92,9 @@ def do_disconnect(strategy, user, association_id=None, redirect_name='next', partial = partial_pipeline_data(strategy, user, *args, **kwargs) if partial: xargs, xkwargs = partial - response = strategy.disconnect(association_id=association_id, - *xargs, **xkwargs) + if association_id and not xkwargs.get('association_id'): + xkwargs['association_id'] = association_id + response = strategy.disconnect(*xargs, **xkwargs) else: response = strategy.disconnect(user=user, association_id=association_id, From 8593e0c0d7b8cdf048cf3da5739040993d0b681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 27 Mar 2014 22:32:26 -0300 Subject: [PATCH 186/890] Stop tox on first error --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 58c021464..9671e3968 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ envlist = py26, py27, py33, doc [testenv] -commands = nosetests --where=social/tests +commands = nosetests --where=social/tests --stop deps = -r{toxinidir}/social/tests/requirements.txt [testenv:doc] From 3bb48a18d393c509e2c2ec54481fe3bd4c98ed98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 28 Mar 2014 14:25:47 -0300 Subject: [PATCH 187/890] Make exception raise optional with setting. Add tests and docs --- docs/backends/bitbucket.rst | 14 ++++++++++++ social/backends/bitbucket.py | 29 +++++++++++++++---------- social/tests/backends/test_bitbucket.py | 26 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/docs/backends/bitbucket.rst b/docs/backends/bitbucket.rst index aa152f224..a66478b26 100644 --- a/docs/backends/bitbucket.rst +++ b/docs/backends/bitbucket.rst @@ -10,3 +10,17 @@ Bitbucket works similar to Twitter OAuth. SOCIAL_AUTH_BITBUCKET_KEY = '' SOCIAL_AUTH_BITBUCKET_SECRET = '' + + + +Settings +-------- + +Sometimes Bitbucket users don't have a verified email address, making it +impossible to get the basic user information to continue the auth process. +It's possible to avoid these users with this setting:: + + SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY = True + +By default the setting is set to ``False`` since it's possible for a project to +gather this information by other methods. diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index a7797c946..162c32716 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -2,8 +2,8 @@ Bitbucket OAuth1 backend, docs at: http://psa.matiasaguirre.net/docs/backends/bitbucket.html """ -from social.backends.oauth import BaseOAuth1 from social.exceptions import AuthForbidden +from social.backends.oauth import BaseOAuth1 class BitbucketOAuth(BaseOAuth1): @@ -23,12 +23,12 @@ class BitbucketOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Bitbucket account""" - return {'username': response.get('username'), - 'email': response.get('email'), - 'fullname': ' '.join((response.get('first_name'), - response.get('last_name'))), - 'first_name': response.get('first_name'), - 'last_name': response.get('last_name')} + return {'username': response.get('username') or '', + 'email': response.get('email') or '', + 'fullname': ' '.join((response.get('first_name') or '', + response.get('last_name') or '')), + 'first_name': response.get('first_name') or '', + 'last_name': response.get('last_name') or ''} def user_data(self, access_token): """Return user data provided""" @@ -44,8 +44,13 @@ def user_data(self, access_token): email = address['email'] if address['primary']: break - if email is None: - raise AuthForbidden(self, "Bitbucket account has any verified email") - return dict(self.get_json('https://bitbucket.org/api/1.0/users/' + - email)['user'], - email=email) + + if email: + return dict(self.get_json('https://bitbucket.org/api/1.0/users/' + + email)['user'], + email=email) + elif self.setting('VERIFIED_EMAILS_ONLY', False): + raise AuthForbidden(self, + 'Bitbucket account has any verified email') + else: + return {} diff --git a/social/tests/backends/test_bitbucket.py b/social/tests/backends/test_bitbucket.py index 548a13d7e..c59b9e577 100644 --- a/social/tests/backends/test_bitbucket.py +++ b/social/tests/backends/test_bitbucket.py @@ -2,6 +2,7 @@ from httpretty import HTTPretty from social.p3 import urlencode +from social.exceptions import AuthForbidden from social.tests.backends.oauth import OAuth1Test @@ -46,4 +47,29 @@ def test_login(self): self.do_login() def test_partial_pipeline(self): + HTTPretty.register_uri(HTTPretty.GET, + 'https://bitbucket.org/api/1.0/emails/', + status=200, body=self.emails_body) self.do_partial_pipeline() + + +class BitbucketOAuth1FailTest(BitbucketOAuth1Test): + emails_body = json.dumps([{ + 'active': False, + 'email': 'foo@bar.com', + 'primary': True + }]) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY': True + }) + super(BitbucketOAuth1FailTest, self).test_login \ + .when.called_with().should.throw(AuthForbidden) + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY': True + }) + super(BitbucketOAuth1FailTest, self).test_partial_pipeline \ + .when.called_with().should.throw(AuthForbidden) From 7d41cd8851899e38ef5191c56dcabd9ed7be79b6 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 26 Mar 2014 12:28:50 +0100 Subject: [PATCH 188/890] Added backend for Last.Fm. There is probably an easier way to implement this. --- social/backends/lastfm.py | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 social/backends/lastfm.py diff --git a/social/backends/lastfm.py b/social/backends/lastfm.py new file mode 100644 index 000000000..d121c2747 --- /dev/null +++ b/social/backends/lastfm.py @@ -0,0 +1,74 @@ +from social.backends.base import BaseAuth +from social.backends.oauth import BaseOAuth1 +from social.exceptions import AuthException +import hashlib +import json +import logging +import urllib + +class LastFmAuth(BaseAuth): + """Last.Fm authentication backend. Requires two settings: LASTFM_API_KEY and LASTFM_SECRET. + Don't forget to set the Last.fm callback to something sensible (http://your.site/lastfm/complete).""" + # Similar to OAuth. + # 1. Server redirects user to http://www.last.fm/api/auth with the api key (LASTFM_API_KEY). + # 2. Last.fm asks user to authorize. If user agrees, user is redirected to a callback url (/lastfm/complete) with a temporary token. + # 3. Server builds a signed request using the temporary token and sends it to Last.fm. + # 4. Last.fm responds with a username and session key. + # 5. User is logged in. + # The session key can be used by the server to make signed requests on behalf of the user. + + name = "lastfm" + EXTRA_DATA = [ + ('key', 'session_key') + ] + + def auth_url(self): + return "http://www.last.fm/api/auth/?api_key={api_key}".format(api_key=self.setting('LASTFM_API_KEY')) + + def auth_complete(self, *args, **kwargs): + """Completes login process, must return user instance""" + + # Sign session request. + signature_base = "api_key{api_key}methodauth.getSessiontoken{token}{secret}".format( + api_key=self.setting('LASTFM_API_KEY'), + token=self.data['token'], + #secret=settings.LASTFM_SECRET, + secret=self.setting('LASTFM_SECRET'), + ) + signature = hashlib.md5(signature_base).hexdigest() + logging.debug("Generated signature {signature} from {signature_base}".format(signature=signature, signature_base=signature_base)) + + session_key_request_template = "http://ws.audioscrobbler.com/2.0/?method=auth.getSession&api_key={api_key}&token={token}&api_sig={api_sig}&format=json" + session_key_request = session_key_request_template.format( + api_key=self.setting('LASTFM_API_KEY'), + token=self.data['token'], + api_sig=signature, + ) + urlopener = urllib.FancyURLopener() + logging.debug("Requesting session key for token {token} using signature {signature}".format(token=self.data['token'], signature=signature)) + # {"session":{"name":"xxxxxxxx","key":"xxxxxxxxx","subscriber":"0"}} + session_request_response = urlopener.open(session_key_request, signature).read() + logging.debug("Last.fm responded with {response} for session key request {request}".format( + response=session_request_response, + request=session_key_request, + )) + + kwargs.update({'response': json.loads(session_request_response)['session'], 'backend': self}) + return self.strategy.authenticate(*args, **kwargs) + + def get_user_id(self, details, response): + """Return a unique ID for the current user, by default from server + response.""" + return response.get('name') + + def get_user_details(self, response): + """ + """ + return { + 'username': response['name'], + 'email': '', + 'fullname': response['name'], + 'first_name': '', + 'last_name': '', + } + From 20425933efadd66a2e5027f6fa382d0e5a8ae561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 1 Apr 2014 10:09:54 -0300 Subject: [PATCH 189/890] Refactor Last.fm backend (simplify code) --- social/backends/lastfm.py | 78 +++++++++++++++------------------------ 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/social/backends/lastfm.py b/social/backends/lastfm.py index d121c2747..565855089 100644 --- a/social/backends/lastfm.py +++ b/social/backends/lastfm.py @@ -1,59 +1,44 @@ -from social.backends.base import BaseAuth -from social.backends.oauth import BaseOAuth1 -from social.exceptions import AuthException import hashlib -import json -import logging -import urllib -class LastFmAuth(BaseAuth): - """Last.Fm authentication backend. Requires two settings: LASTFM_API_KEY and LASTFM_SECRET. - Don't forget to set the Last.fm callback to something sensible (http://your.site/lastfm/complete).""" - # Similar to OAuth. - # 1. Server redirects user to http://www.last.fm/api/auth with the api key (LASTFM_API_KEY). - # 2. Last.fm asks user to authorize. If user agrees, user is redirected to a callback url (/lastfm/complete) with a temporary token. - # 3. Server builds a signed request using the temporary token and sends it to Last.fm. - # 4. Last.fm responds with a username and session key. - # 5. User is logged in. - # The session key can be used by the server to make signed requests on behalf of the user. +from social.backends.base import BaseAuth - name = "lastfm" + +class LastFmAuth(BaseAuth): + """ + Last.Fm authentication backend. Requires two settings: + SOCIAL_AUTH_LASTFM_KEY + SOCIAL_AUTH_LASTFM_SECRET + + Don't forget to set the Last.fm callback to something sensible like + http://your.site/lastfm/complete + """ + name = 'lastfm' + AUTH_URL = 'http://www.last.fm/api/auth/?api_key={api_key}' EXTRA_DATA = [ ('key', 'session_key') ] def auth_url(self): - return "http://www.last.fm/api/auth/?api_key={api_key}".format(api_key=self.setting('LASTFM_API_KEY')) + return self.AUTH_URL.format(api_key=self.setting('KEY')) def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" - - # Sign session request. - signature_base = "api_key{api_key}methodauth.getSessiontoken{token}{secret}".format( - api_key=self.setting('LASTFM_API_KEY'), - token=self.data['token'], - #secret=settings.LASTFM_SECRET, - secret=self.setting('LASTFM_SECRET'), - ) - signature = hashlib.md5(signature_base).hexdigest() - logging.debug("Generated signature {signature} from {signature_base}".format(signature=signature, signature_base=signature_base)) - - session_key_request_template = "http://ws.audioscrobbler.com/2.0/?method=auth.getSession&api_key={api_key}&token={token}&api_sig={api_sig}&format=json" - session_key_request = session_key_request_template.format( - api_key=self.setting('LASTFM_API_KEY'), - token=self.data['token'], - api_sig=signature, - ) - urlopener = urllib.FancyURLopener() - logging.debug("Requesting session key for token {token} using signature {signature}".format(token=self.data['token'], signature=signature)) - # {"session":{"name":"xxxxxxxx","key":"xxxxxxxxx","subscriber":"0"}} - session_request_response = urlopener.open(session_key_request, signature).read() - logging.debug("Last.fm responded with {response} for session key request {request}".format( - response=session_request_response, - request=session_key_request, - )) - - kwargs.update({'response': json.loads(session_request_response)['session'], 'backend': self}) + key, secret = self.get_key_and_secret() + token = self.data['token'] + + signature = hashlib.md5(''.join( + ('api_key', key, 'methodauth.getSession', 'token', token, secret) + ).encode()).hexdigest() + + response = self.get_json('http://ws.audioscrobbler.com/2.0/', data={ + 'method': 'auth.getSession', + 'api_key': key, + 'token': token, + 'api_sig': signature, + 'format': 'json' + }, method='POST') + + kwargs.update({'response': response['session'], 'backend': self}) return self.strategy.authenticate(*args, **kwargs) def get_user_id(self, details, response): @@ -62,8 +47,6 @@ def get_user_id(self, details, response): return response.get('name') def get_user_details(self, response): - """ - """ return { 'username': response['name'], 'email': '', @@ -71,4 +54,3 @@ def get_user_details(self, response): 'first_name': '', 'last_name': '', } - From d3e27d8039fb6a3fab60fc23456d4074773f15bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 1 Apr 2014 10:10:12 -0300 Subject: [PATCH 190/890] Last.fm docs --- docs/backends/index.rst | 1 + docs/backends/lastfm.rst | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 docs/backends/lastfm.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index a5526df5a..5c79a5013 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -71,6 +71,7 @@ Social backends google instagram jawbone + lastfm linkedin livejournal live diff --git a/docs/backends/lastfm.rst b/docs/backends/lastfm.rst new file mode 100644 index 000000000..0dc7e1b30 --- /dev/null +++ b/docs/backends/lastfm.rst @@ -0,0 +1,17 @@ +Last.fm +======= + +Last.fm uses a similar authentication process than OAuth2 but it's not. In +order to enable the support for it just: + +- Register an application at `Get an API Account`_, set the Last.fm callback to + something sensible like http://your.site/complete/lastfm + +- Fill in the **API Key** and **API Secret** values in your settings:: + + SOCIAL_AUTH_LASTFM_KEY = '' + SOCIAL_AUTH_LASTFM_SECRET = '' + +- Enable the backend in ``AUTHENTICATION_BACKENDS`` setting. + +.. _Get an API Account: http://www.last.fm/api/account/create From 3303e2a9b18a2f7fea92ee9f34ade66caa00bfd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 1 Apr 2014 10:10:26 -0300 Subject: [PATCH 191/890] Enable Last.fm in example applications --- examples/django_example/example/settings.py | 1 + examples/django_example/example/templates/home.html | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 4395d1063..34b67ec2f 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -187,6 +187,7 @@ 'social.backends.yammer.YammerOAuth2', 'social.backends.yandex.YandexOAuth2', 'social.backends.vimeo.VimeoOAuth1', + 'social.backends.lastfm.LastFmAuth', 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index cdc04b742..abb429430 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -67,6 +67,7 @@ Yandex OAuth2 TAOBAO OAuth2
          Vimeo OAuth1
          +LastFM Auth
          Email Auth
          Username Auth
          From 987516c5ea2c9533de3186653988461ad4f897da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 1 Apr 2014 15:44:36 -0300 Subject: [PATCH 192/890] Switch custom redirect state to off in mendeley OAuth2. Closes #234 --- social/backends/mendeley.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index 3e15ef21f..48464e76b 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -52,6 +52,7 @@ class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): ACCESS_TOKEN_URL = 'https://api-oauth2.mendeley.com/oauth/token' ACCESS_TOKEN_METHOD = 'POST' DEFAULT_SCOPE = ['all'] + REDIRECT_STATE = False EXTRA_DATA = MendeleyMixin.EXTRA_DATA + [ ('refresh_token', 'refresh_token'), ('expires_in', 'expires_in'), From f78879e51b63af9e9254ecac2a627db85a8b058b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 1 Apr 2014 20:30:11 -0300 Subject: [PATCH 193/890] Fix use-case snippet --- docs/use_cases.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index a2eee8d3f..ac808a46b 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -109,7 +109,7 @@ Signup by OAuth access_token It's a common scenario that mobile applications will use an SDK to signup a user withing the app, but that signup won't be reflected by -python-socia-auth_ unless the corresponding database entries are created. In +python-social-auth_ unless the corresponding database entries are created. In order to do so, it's possible to create a view / route that creates those entries by a given ``access_token``. Take the following code for instance (the code follows Django conventions, but versions for others frameworks can be @@ -129,6 +129,7 @@ implemented easily):: def register_by_access_token(request, backend): # This view expects an access_token GET parameter token = request.GET.get('access_token') + backend = request.strategy.backend user = backend.do_auth(request.GET.get('access_token')) if user: login(request, user) From a153a0fe682a4581c6d0327235fdcbc4194b8b82 Mon Sep 17 00:00:00 2001 From: Damien Date: Tue, 1 Apr 2014 20:46:24 -0400 Subject: [PATCH 194/890] Incorrect syntax given in the documention --- docs/configuration/porting_from_dsa.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/porting_from_dsa.rst b/docs/configuration/porting_from_dsa.rst index 6254cb2b6..665e7db89 100644 --- a/docs/configuration/porting_from_dsa.rst +++ b/docs/configuration/porting_from_dsa.rst @@ -49,11 +49,11 @@ the ``social`` namespace. Replace the old include with:: On templates use a namespaced URL:: - {% url social:begin "google-oauth2" %} + {% url 'social:begin' "google-oauth2" %} Account disconnection URL would be:: - {% url social:disconnect_individual provider, id %} + {% url 'social:disconnect_individual' provider, id %} Porting settings From 209de8a8d587523d3f3a24dfe55c8e43003a3ce3 Mon Sep 17 00:00:00 2001 From: Krishan Gupta Date: Tue, 1 Apr 2014 17:58:49 -0700 Subject: [PATCH 195/890] Update settings.rst --- docs/configuration/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 2e3c34c89..14d5cfbf1 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -4,7 +4,7 @@ Configuration Application setup ----------------- -Once the application was installed (check Installation_) define the following +Once the application is installed (check Installation_) define the following settings to enable the application behavior. Also check the sections dedicated to each framework for detailed instructions. From 7e9b69752167eb7894ca809f028234857fa9619f Mon Sep 17 00:00:00 2001 From: Joe Hura Date: Wed, 2 Apr 2014 16:40:58 +1100 Subject: [PATCH 196/890] Add support for Vimeo OAuth 2 as part of Vimeo API v3 --- social/backends/vimeo.py | 55 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/social/backends/vimeo.py b/social/backends/vimeo.py index a6a61ec2f..f08b637e9 100644 --- a/social/backends/vimeo.py +++ b/social/backends/vimeo.py @@ -1,4 +1,4 @@ -from social.backends.oauth import BaseOAuth1 +from social.backends.oauth import BaseOAuth1, BaseOAuth2 class VimeoOAuth1(BaseOAuth1): @@ -32,3 +32,56 @@ def user_data(self, access_token, *args, **kwargs): params={'format': 'json', 'method': 'vimeo.people.getInfo'}, auth=self.oauth_auth(access_token) ) + + +class VimeoOAuth2(BaseOAuth2): + """Vimeo OAuth2 authentication backend""" + name = 'vimeo-oauth2' + AUTHORIZATION_URL = 'https://api.vimeo.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://api.vimeo.com/oauth/access_token' + REFRESH_TOKEN_URL = 'https://api.vimeo.com/oauth/request_token' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' + + API_ACCEPT_HEADER = {'Accept' : 'application/vnd.vimeo.*+json;version=3.0'} + + def get_redirect_uri(self, state=None): + """ + Build redirect with redirect_state parameter. + + @Vimeo API 3 requires exact redirect uri without additional + additional state parameter included + """ + return self.redirect_uri + + def get_user_id(self, details, response): + """Return user id""" + try: + user_id = response.get('user', {})['uri'].split('/')[-1] + except KeyError: + user_id = None + return user_id + + def get_user_details(self, response): + """Return user details from account""" + user = response.get('user', {}) + fullname = user.get('name', '') + + if ' ' in fullname: + first_name, last_name = fullname.split(' ', 1) + else: + first_name, last_name = fullname, '' + + return {'username': fullname, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name,} + + def user_data(self, access_token, *args, **kwargs): + """Return user data provided""" + return self.get_json( + 'https://api.vimeo.com/me', + params={'access_token' : access_token}, + headers=VimeoOAuth2.API_ACCEPT_HEADER, + ) + From 7a71f935742b64805e1ea66aad7fcfea7c0cd217 Mon Sep 17 00:00:00 2001 From: "(cdep) illabout" Date: Wed, 2 Apr 2014 22:23:36 +0900 Subject: [PATCH 197/890] Fix small spelling mistake. --- docs/configuration/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 14d5cfbf1..18ba3658c 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -97,7 +97,7 @@ User model ---------- ``UserSocialAuth`` instances keep a reference to the ``User`` model of your -project, since this is not know, the ``User`` model must be configured by +project, since this is not known, the ``User`` model must be configured by a setting:: SOCIAL_AUTH_USER_MODEL = 'foo.bar.User' From b33b9c4eb3bd2e7582ea2c8180db7374fea9ffb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 3 Apr 2014 13:10:02 -0300 Subject: [PATCH 198/890] Include strava backend in the index --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 5c79a5013..5b782bcdb 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -95,6 +95,7 @@ Social backends stackoverflow steam stocktwits + strava stripe taobao thisismyjam From c5dd3339ff92f4b3e9da188a0e34c3c0483077da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 3 Apr 2014 13:19:14 -0300 Subject: [PATCH 199/890] Option for open id providers to specify the username key in the values --- docs/backends/openid.rst | 14 ++++++++++++++ social/backends/fedora.py | 1 + social/backends/open_id.py | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/backends/openid.rst b/docs/backends/openid.rst index f55e9d7e1..0ac806188 100644 --- a/docs/backends/openid.rst +++ b/docs/backends/openid.rst @@ -28,5 +28,19 @@ is to avoid replacing old (needed) values when they don't form part of current response. If not present, then this check is avoided and the value will replace any data. +Username +-------- + +The OpenId_ backend will check for a ``username`` key in the values returned by +the server, but default to ``first-name`` + ``last-name`` if that key is +missing. It's possible to indicate the username key in the values If the +username is under a different key with a setting, but backends should have +defined a default value. For example:: + + SOCIAL_AUTH_FEDORA_USERNAME_KEY = 'nickname' + +This setting indicates that the username should be populated by the +``nickname`` value in the Fedora OpenId_ provider. + .. _OpenId: http://openid.net/ .. _OAuth: http://oauth.net/ diff --git a/social/backends/fedora.py b/social/backends/fedora.py index 38ed9bfbd..a4b7ff644 100644 --- a/social/backends/fedora.py +++ b/social/backends/fedora.py @@ -8,3 +8,4 @@ class FedoraOpenId(OpenIdAuth): name = 'fedora' URL = 'https://id.fedoraproject.org' + USERNAME_KEY = 'nickname' diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 11e333315..464bdb1fb 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -36,6 +36,7 @@ class OpenIdAuth(BaseAuth): """Generic OpenID authentication backend""" name = 'openid' URL = None + USERNAME_KEY = 'username' def get_user_id(self, details, response): """Return user unique id provided by service""" @@ -89,9 +90,10 @@ def get_user_details(self, response): except ValueError: last_name = fullname + username_key = self.setting('USERNAME_KEY') or self.USERNAME_KEY values.update({'fullname': fullname, 'first_name': first_name, 'last_name': last_name, - 'username': values.get('username') or + 'username': values.get(username_key) or (first_name.title() + last_name.title())}) return values From ec22909b7b63c67e5ce5cb4116222dd456b97414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 4 Apr 2014 03:09:08 -0300 Subject: [PATCH 200/890] Remove doc about deprecated setting. Refs #241 --- docs/configuration/settings.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 18ba3658c..6b9bf2222 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -142,10 +142,6 @@ defaults to generating one if needed. An UUID is appended to usernames in case of collisions. Here are some settings to control usernames generation. -``SOCIAL_AUTH_DEFAULT_USERNAME = 'foobar'`` - Default value to use as username, can be a callable. An UUID will be - appended in case of duplicate entries. - ``SOCIAL_AUTH_UUID_LENGTH = 16`` This controls the length of the UUID appended to usernames. From 4eb32df8a9fdd7b370eb427adbc97b39f5890e53 Mon Sep 17 00:00:00 2001 From: Alexander Chernigov Date: Tue, 8 Apr 2014 16:02:26 +0300 Subject: [PATCH 201/890] Handle properly refusing when entering via twitter --- social/backends/twitter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index dc3ac8627..ca351f813 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -3,6 +3,7 @@ http://psa.matiasaguirre.net/docs/backends/twitter.html """ from social.backends.oauth import BaseOAuth1 +from social.exceptions import AuthCanceled class TwitterOAuth(BaseOAuth1): @@ -13,6 +14,12 @@ class TwitterOAuth(BaseOAuth1): REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' + def process_error(self, data): + if 'denied' in data: + raise AuthCanceled(self) + else: + super(TwitterOAuth, self).process_error(data) + def get_user_details(self, response): """Return user details from Twitter account""" try: From 1c74cbd9c42a7d753e4ef0e4e62fc32fd4c532ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 10 Apr 2014 17:37:42 -0300 Subject: [PATCH 202/890] Remove unused parameters from pipeline prototypes --- social/pipeline/mail.py | 3 +-- social/pipeline/user.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index 73dd3599e..a8677210c 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -3,8 +3,7 @@ @partial -def mail_validation(strategy, details, user=None, is_new=False, - *args, **kwargs): +def mail_validation(strategy, details, *args, **kwargs): requires_validation = strategy.backend.REQUIRES_EMAIL_VALIDATION or \ strategy.setting('FORCE_EMAIL_VALIDATION', False) if requires_validation and details.get('email'): diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 647c5653a..9f20cb0ce 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -53,7 +53,7 @@ def get_username(strategy, details, user=None, *args, **kwargs): return {'username': final_username} -def create_user(strategy, details, response, uid, user=None, *args, **kwargs): +def create_user(strategy, details, user=None, *args, **kwargs): if user: return {'is_new': False} @@ -69,7 +69,7 @@ def create_user(strategy, details, response, uid, user=None, *args, **kwargs): } -def user_details(strategy, details, response, user=None, *args, **kwargs): +def user_details(strategy, details, user=None, *args, **kwargs): """Update user details using data from provider.""" if user: changed = False # flag to track changes From d4880f0dd7d5152d17e206908e7058aa8e4629fb Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Mon, 14 Apr 2014 16:50:58 +0200 Subject: [PATCH 203/890] Add Twitch backend --- docs/backends/index.rst | 1 + docs/backends/twitch.rst | 18 +++++++++++++++ docs/intro.rst | 2 ++ social/backends/twitch.py | 25 ++++++++++++++++++++ social/tests/backends/test_twitch.py | 34 ++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 docs/backends/twitch.rst create mode 100644 social/backends/twitch.py create mode 100644 social/tests/backends/test_twitch.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 5b782bcdb..8a43ac91f 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -103,6 +103,7 @@ Social backends tripit tumblr twilio + twitch twitter vimeo vk diff --git a/docs/backends/twitch.rst b/docs/backends/twitch.rst new file mode 100644 index 000000000..c453f83bd --- /dev/null +++ b/docs/backends/twitch.rst @@ -0,0 +1,18 @@ +Twitch +====== + +Twitch works similar to Facebook (OAuth). + +- Register a new application in the `connections tab`_ of your Twitch settings page, set the callback URL to + ``http://example.com/complete/twitch/`` replacing ``example.com`` with your domain. + +- Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_TWITCH_KEY = '' + SOCIAL_AUTH_TWITCH_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_TWITCH_SCOPE = [...] + +.. _connections tab: http://www.twitch.tv/settings/connections diff --git a/docs/intro.rst b/docs/intro.rst index d68844573..b1335e8eb 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -74,6 +74,7 @@ or extend current one): * Tripit_ OAuth1 * Tumblr_ OAuth1 * Twilio_ Auth + * Twitch_ OAuth2 * Twitter_ OAuth1 * Vimeo_ OAuth1 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications @@ -142,6 +143,7 @@ section. .. _Stripe: https://stripe.com .. _Tripit: https://www.tripit.com .. _Twilio: https://www.twilio.com +.. _Twitch: http://www.twitch.tv/ .. _Twitter: http://twitter.com .. _VK.com: http://vk.com .. _Weibo: http://weibo.com diff --git a/social/backends/twitch.py b/social/backends/twitch.py new file mode 100644 index 000000000..a0a8ddf9e --- /dev/null +++ b/social/backends/twitch.py @@ -0,0 +1,25 @@ +""" +Twitch OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/twitch.html +""" +from social.backends.oauth import BaseOAuth2 + + +class TwitchOAuth2(BaseOAuth2): + """Twitch OAuth authentication backend""" + name = 'twitch' + ID_KEY = '_id' + AUTHORIZATION_URL = 'https://api.twitch.tv/kraken/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://api.twitch.tv/kraken/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + DEFAULT_SCOPE = ['user_read'] + REDIRECT_STATE = False + + def get_user_details(self, response): + return {'username': response.get('name'), 'email': response.get('email'), + 'first_name': '', 'last_name': ''} + + def user_data(self, access_token, *args, **kwargs): + url = 'https://api.twitch.tv/kraken/user/' + user_data = self.get_json(url, params={'oauth_token': access_token}) + return user_data diff --git a/social/tests/backends/test_twitch.py b/social/tests/backends/test_twitch.py new file mode 100644 index 000000000..ce8bded72 --- /dev/null +++ b/social/tests/backends/test_twitch.py @@ -0,0 +1,34 @@ +import json +from social.tests.backends.oauth import OAuth2Test + + +class TwitchOAuth2Test(OAuth2Test): + backend_path = 'social.backends.twitch.TwitchOAuth2' + user_data_url = 'https://api.twitch.tv/kraken/user/' + expected_username = 'test_user1' + access_token_body = json.dumps({ + 'access_token': 'foobar', + }) + + user_data_body = json.dumps({ + "type": "user", + "name": "test_user1", + "created_at": "2011-06-03T17:49:19Z", + "updated_at": "2012-06-18T17:19:57Z", + "_links": { + "self": "https://api.twitch.tv/kraken/users/test_user1" + }, + "logo": "http://static-cdn.jtvnw.net/jtv_user_pictures/test_user1-profile_image-62e8318af864d6d7-300x300.jpeg", + "_id": 22761313, + "display_name": "test_user1", + "email": "asdf@asdf.com", + "partnered": True, + "bio": "test bio woo I'm a test user" + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() + From 461ae02c612a3c44212414f3147d020330cbd40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 14 Apr 2014 13:03:51 -0300 Subject: [PATCH 204/890] PEP8 --- docs/backends/twitch.rst | 5 +++-- social/backends/twitch.py | 15 ++++++++++----- social/tests/backends/test_twitch.py | 27 +++++++++++++-------------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/backends/twitch.rst b/docs/backends/twitch.rst index c453f83bd..bb99a9e2c 100644 --- a/docs/backends/twitch.rst +++ b/docs/backends/twitch.rst @@ -3,8 +3,9 @@ Twitch Twitch works similar to Facebook (OAuth). -- Register a new application in the `connections tab`_ of your Twitch settings page, set the callback URL to - ``http://example.com/complete/twitch/`` replacing ``example.com`` with your domain. +- Register a new application in the `connections tab`_ of your Twitch settings + page, set the callback URL to ``http://example.com/complete/twitch/`` + replacing ``example.com`` with your domain. - Fill ``Client Id`` and ``Client Secret`` values in the settings:: diff --git a/social/backends/twitch.py b/social/backends/twitch.py index a0a8ddf9e..efa995d56 100644 --- a/social/backends/twitch.py +++ b/social/backends/twitch.py @@ -16,10 +16,15 @@ class TwitchOAuth2(BaseOAuth2): REDIRECT_STATE = False def get_user_details(self, response): - return {'username': response.get('name'), 'email': response.get('email'), - 'first_name': '', 'last_name': ''} + return { + 'username': response.get('name'), + 'email': response.get('email'), + 'first_name': '', + 'last_name': '' + } def user_data(self, access_token, *args, **kwargs): - url = 'https://api.twitch.tv/kraken/user/' - user_data = self.get_json(url, params={'oauth_token': access_token}) - return user_data + return self.get_json( + 'https://api.twitch.tv/kraken/user/', + params={'oauth_token': access_token} + ) diff --git a/social/tests/backends/test_twitch.py b/social/tests/backends/test_twitch.py index ce8bded72..a28a17883 100644 --- a/social/tests/backends/test_twitch.py +++ b/social/tests/backends/test_twitch.py @@ -9,21 +9,21 @@ class TwitchOAuth2Test(OAuth2Test): access_token_body = json.dumps({ 'access_token': 'foobar', }) - user_data_body = json.dumps({ - "type": "user", - "name": "test_user1", - "created_at": "2011-06-03T17:49:19Z", - "updated_at": "2012-06-18T17:19:57Z", - "_links": { - "self": "https://api.twitch.tv/kraken/users/test_user1" + 'type': 'user', + 'name': 'test_user1', + 'created_at': '2011-06-03T17:49:19Z', + 'updated_at': '2012-06-18T17:19:57Z', + '_links': { + 'self': 'https://api.twitch.tv/kraken/users/test_user1' }, - "logo": "http://static-cdn.jtvnw.net/jtv_user_pictures/test_user1-profile_image-62e8318af864d6d7-300x300.jpeg", - "_id": 22761313, - "display_name": "test_user1", - "email": "asdf@asdf.com", - "partnered": True, - "bio": "test bio woo I'm a test user" + 'logo': 'http://static-cdn.jtvnw.net/jtv_user_pictures/' + 'test_user1-profile_image-62e8318af864d6d7-300x300.jpeg', + '_id': 22761313, + 'display_name': 'test_user1', + 'email': 'asdf@asdf.com', + 'partnered': True, + 'bio': 'test bio woo I\'m a test user' }) def test_login(self): @@ -31,4 +31,3 @@ def test_login(self): def test_partial_pipeline(self): self.do_partial_pipeline() - From 5309c604fd51b64b7dc7d4b0a1916b27d0c9007e Mon Sep 17 00:00:00 2001 From: David Blado Date: Mon, 14 Apr 2014 22:51:56 +0000 Subject: [PATCH 205/890] linkedin now requires redirect uris to be verified: https://developer.linkedin.com/blog/register-your-oauth-2-redirect-urls --- social/backends/linkedin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index fb56731b5..634820cad 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -75,6 +75,7 @@ class LinkedinOAuth2(BaseLinkedinAuth, BaseOAuth2): AUTHORIZATION_URL = 'https://www.linkedin.com/uas/oauth2/authorization' ACCESS_TOKEN_URL = 'https://www.linkedin.com/uas/oauth2/accessToken' ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False def user_data(self, access_token, *args, **kwargs): return self.get_json( From fa1b95f3c48284a4748cf2d66e366c5cf7c8a11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 18 Apr 2014 10:19:55 -0300 Subject: [PATCH 206/890] Switch VK OpenAPI to session intead of cookies. Refs #250 --- social/backends/vk.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/social/backends/vk.py b/social/backends/vk.py index e676f36ba..526c12cb4 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -6,6 +6,7 @@ from time import time from hashlib import md5 +from social.utils import parse_qs from social.backends.base import BaseAuth from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthTokenRevoked, AuthException @@ -44,25 +45,24 @@ def auth_html(self): def auth_complete(self, *args, **kwargs): """Performs check of authentication in VKontakte, returns User if succeeded""" - app_cookie = 'vk_app_' + self.setting('APP_ID') - - if not 'id' in self.data or not self.strategy.cookie_get(app_cookie): + session_value = self.strategy.session_get( + 'vk_app_' + self.setting('APP_ID') + ) + if 'id' not in self.data or not session_value: raise ValueError('VK.com authentication is not completed') - key, secret = self.get_key_and_secret() - cookie_dict = dict(item.split('=') for item in - self.strategy.cookie_get(app_cookie).split('&')) - check_str = ''.join(item + '=' + cookie_dict[item] + mapping = parse_qs(session_value) + check_str = ''.join(item + '=' + mapping[item] for item in ['expire', 'mid', 'secret', 'sid']) + key, secret = self.get_key_and_secret() hash = md5((check_str + secret).encode('utf-8')).hexdigest() + if hash != mapping['sig'] or int(mapping['expire']) < time(): + raise ValueError('VK.com authentication failed: Invalid Hash') - if hash != cookie_dict['sig'] or int(cookie_dict['expire']) < time(): - raise ValueError('VK.com authentication failed: invalid hash') - else: - kwargs.update({'backend': self, - 'response': self.user_data(cookie_dict['mid'])}) - return self.strategy.authenticate(*args, **kwargs) + kwargs.update({'backend': self, + 'response': self.user_data(mapping['mid'])}) + return self.strategy.authenticate(*args, **kwargs) def uses_redirect(self): """VK.com does not require visiting server url in order From 594735b027803e004354555d278f67647c885eaf Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 19 Apr 2014 13:57:01 +0200 Subject: [PATCH 207/890] User model fields accessors clashes issue solved --- examples/django_example/example/app/models.py | 4 ++-- examples/django_example/example/settings.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/django_example/example/app/models.py b/examples/django_example/example/app/models.py index 5bcf78eb7..415d74896 100644 --- a/examples/django_example/example/app/models.py +++ b/examples/django_example/example/app/models.py @@ -1,6 +1,6 @@ # Define a custom User class to work with django-social-auth -from django.contrib.auth.models import AbstractUser, UserManager +from django.contrib.auth.models import AbstractUser class CustomUser(AbstractUser): - objects = UserManager() + pass diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 34b67ec2f..3eab0b4f3 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -193,6 +193,8 @@ 'django.contrib.auth.backends.ModelBackend', ) +AUTH_USER_MODEL = 'app.CustomUser' + LOGIN_URL = '/login/' LOGIN_REDIRECT_URL = '/done/' URL_PATH = '' From 61b484895d3c12d9a64fccf8516987379d237bb8 Mon Sep 17 00:00:00 2001 From: Serg Baburin Date: Wed, 23 Apr 2014 15:18:13 +0400 Subject: [PATCH 208/890] Using https as required by the API. --- social/backends/yahoo.py | 4 ++-- social/tests/backends/test_yahoo.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 4f0873e52..c1590a527 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -41,7 +41,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - url = 'http://social.yahooapis.com/v1/user/{0}/profile?format=json' + url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json' return self.get_json( url.format(self._get_guid(access_token)), auth=self.oauth_auth(access_token) @@ -53,6 +53,6 @@ def _get_guid(self, access_token): it's also returned during one of OAuth calls """ return self.get_json( - 'http://social.yahooapis.com/v1/me/guid?format=json', + 'https://social.yahooapis.com/v1/me/guid?format=json', auth=self.oauth_auth(access_token) )['guid']['value'] diff --git a/social/tests/backends/test_yahoo.py b/social/tests/backends/test_yahoo.py index 8259ba499..d3c87ae77 100644 --- a/social/tests/backends/test_yahoo.py +++ b/social/tests/backends/test_yahoo.py @@ -7,7 +7,7 @@ class YahooOAuth1Test(OAuth1Test): backend_path = 'social.backends.yahoo.YahooOAuth' - user_data_url = 'http://social.yahooapis.com/v1/user/a-guid/profile?' \ + user_data_url = 'https://social.yahooapis.com/v1/user/a-guid/profile?' \ 'format=json' expected_username = 'foobar' access_token_body = json.dumps({ @@ -21,7 +21,7 @@ class YahooOAuth1Test(OAuth1Test): }) guid_body = json.dumps({ 'guid': { - 'uri': 'http://social.yahooapis.com/v1/me/guid', + 'uri': 'https://social.yahooapis.com/v1/me/guid', 'value': 'a-guid' } }) @@ -37,7 +37,7 @@ class YahooOAuth1Test(OAuth1Test): 'height': 192 }, 'created': '2013-03-18T04:15:08Z', - 'uri': 'http://social.yahooapis.com/v1/user/a-guid/profile', + 'uri': 'https://social.yahooapis.com/v1/user/a-guid/profile', 'isConnected': False, 'profileUrl': 'http://profile.yahoo.com/a-guid', 'guid': 'a-guid', @@ -48,7 +48,7 @@ class YahooOAuth1Test(OAuth1Test): def test_login(self): HTTPretty.register_uri( HTTPretty.GET, - 'http://social.yahooapis.com/v1/me/guid?format=json', + 'https://social.yahooapis.com/v1/me/guid?format=json', status=200, body=self.guid_body ) From c3bd434c2f399e252c6c0eee6230f17c3819a280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 23 Apr 2014 21:55:05 -0300 Subject: [PATCH 209/890] Refactor fullname, first name and last name generation. Fixes #240 --- social/backends/amazon.py | 6 ++---- social/backends/angel.py | 4 ++-- social/backends/appsfuel.py | 8 ++++++-- social/backends/base.py | 14 ++++++++++++++ social/backends/behance.py | 9 ++++++--- social/backends/bitbucket.py | 11 +++++++---- social/backends/box.py | 7 ++++++- social/backends/clef.py | 9 +++++++-- social/backends/coinbase.py | 7 +++---- social/backends/docker.py | 7 ++++++- social/backends/douban.py | 7 ++++++- social/backends/dropbox.py | 14 ++++++++++++-- social/backends/facebook.py | 11 ++++++++--- social/backends/flickr.py | 7 ++++++- social/backends/foursquare.py | 16 ++++++++++------ social/backends/github.py | 14 ++++++++++++-- social/backends/google.py | 12 +++++++++--- social/backends/instagram.py | 8 ++++++-- social/backends/jawbone.py | 7 +++++-- social/backends/lastfm.py | 7 ++++--- social/backends/legacy.py | 14 +++++--------- social/backends/linkedin.py | 8 +++++--- social/backends/live.py | 9 +++++++-- social/backends/mailru.py | 17 +++++++++-------- social/backends/mixcloud.py | 7 ++++--- social/backends/odnoklassniki.py | 22 ++++++++++++++++------ social/backends/orkut.py | 12 +++++++++--- social/backends/pixelpin.py | 8 +++++++- social/backends/podio.py | 5 +++-- social/backends/rdio.py | 11 ++++++++--- social/backends/readability.py | 9 +++++++-- social/backends/runkeeper.py | 7 ++++++- social/backends/skyrock.py | 10 +++++++--- social/backends/soundcloud.py | 9 +++------ social/backends/stackoverflow.py | 7 ++++++- social/backends/stocktwits.py | 10 ++++------ social/backends/strava.py | 6 +++++- social/backends/thisismyjam.py | 12 ++++++++---- social/backends/trello.py | 7 ++++++- social/backends/tripit.py | 8 ++------ social/backends/twitter.py | 8 ++------ social/backends/vimeo.py | 28 ++++++++++------------------ social/backends/vk.py | 21 ++++++++++++++------- social/backends/weibo.py | 7 ++++++- social/backends/xing.py | 7 +++++-- social/backends/yahoo.py | 12 +++++++----- social/backends/yammer.py | 10 ++++++---- social/backends/yandex.py | 16 ++++++++-------- 48 files changed, 322 insertions(+), 170 deletions(-) diff --git a/social/backends/amazon.py b/social/backends/amazon.py index fd2d207a6..c97b8a09f 100644 --- a/social/backends/amazon.py +++ b/social/backends/amazon.py @@ -22,12 +22,10 @@ class AmazonOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from amazon account""" name = response.get('name') or '' - first_name, last_name = name, '' - if name and ' ' in name: - first_name, last_name = name.split(' ', 1) + fullname, first_name, last_name = self.get_user_names(name) return {'username': name, 'email': response.get('email'), - 'fullname': name, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} diff --git a/social/backends/angel.py b/social/backends/angel.py index 738bef3a3..ad364972f 100644 --- a/social/backends/angel.py +++ b/social/backends/angel.py @@ -16,10 +16,10 @@ class AngelOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Angel account""" username = response['angellist_url'].split('/')[-1] - first_name = response['name'].split(' ')[0] - last_name = response['name'].split(' ')[-1] email = response.get('email', '') + fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': username, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'email': email} diff --git a/social/backends/appsfuel.py b/social/backends/appsfuel.py index 691e69a2c..fbe086d64 100644 --- a/social/backends/appsfuel.py +++ b/social/backends/appsfuel.py @@ -15,12 +15,16 @@ class AppsfuelOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Appsfuel account""" - fullname = response.get('display_name', '') email = response.get('email', '') username = email.split('@')[0] if email else '' + fullname, first_name, last_name = self.get_user_names( + response.get('display_name', '') + ) return { 'username': username, - 'first_name': fullname, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'email': email } diff --git a/social/backends/base.py b/social/backends/base.py index 8edd0b1e4..f24a83f0a 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -150,6 +150,20 @@ def get_user_details(self, response): """ raise NotImplementedError('Implement in subclass') + def get_user_names(self, fullname='', first_name='', last_name=''): + # Avoid None values + fullname = fullname or '' + first_name = first_name or '' + last_name = last_name or '' + if fullname and not (first_name or last_name): + try: + first_name, last_name = fullname.split(' ', 1) + except ValueError: + first_name = first_name or fullname or '' + last_name = last_name or '' + fullname = fullname or ' '.join((first_name, last_name)) + return fullname.strip(), first_name.strip(), last_name.strip() + def get_user(self, user_id): """ Return user with given ID from the User model used by this backend. diff --git a/social/backends/behance.py b/social/backends/behance.py index 8f915820a..bf57de001 100644 --- a/social/backends/behance.py +++ b/social/backends/behance.py @@ -21,10 +21,13 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Behance account""" user = response['user'] + fullname, first_name, last_name = self.get_user_names( + user['display_name'], user['first_name'], user['last_name'] + ) return {'username': user['username'], - 'last_name': user['last_name'], - 'first_name': user['first_name'], - 'fullname': user['display_name'], + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'email': ''} def extra_data(self, user, uid, response, details): diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index 162c32716..e0d18e2e2 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -23,12 +23,15 @@ class BitbucketOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Bitbucket account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('first_name', ''), + last_name=response.get('last_name', '') + ) return {'username': response.get('username') or '', 'email': response.get('email') or '', - 'fullname': ' '.join((response.get('first_name') or '', - response.get('last_name') or '')), - 'first_name': response.get('first_name') or '', - 'last_name': response.get('last_name') or ''} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token): """Return user data provided""" diff --git a/social/backends/box.py b/social/backends/box.py index 5b82f5d91..a13eaa26b 100644 --- a/social/backends/box.py +++ b/social/backends/box.py @@ -31,9 +31,14 @@ def do_auth(self, access_token, response=None, *args, **kwargs): def get_user_details(self, response): """Return user details Box.net account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) return {'username': response.get('login'), 'email': response.get('login') or '', - 'first_name': response.get('name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/clef.py b/social/backends/clef.py index f7e09bdd0..7a034db2c 100644 --- a/social/backends/clef.py +++ b/social/backends/clef.py @@ -26,11 +26,16 @@ def auth_params(self, *args, **kwargs): def get_user_details(self, response): """Return user details from Github account""" info = response.get('info') + fullname, first_name, last_name = self.get_user_names( + first_name=info.get('first_name'), + last_name=info.get('last_name') + ) return { 'username': response.get('clef_id'), 'email': info.get('email', ''), - 'first_name': info.get('first_name'), - 'last_name': info.get('last_name'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'phone_number': info.get('phone_number', '') } diff --git a/social/backends/coinbase.py b/social/backends/coinbase.py index d7fd594ce..ae9106f92 100644 --- a/social/backends/coinbase.py +++ b/social/backends/coinbase.py @@ -20,12 +20,11 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Coinbase account""" user_data = response['users'][0]['user'] - name = user_data['name'] - name_split = name.split() - first_name = name_split[0] - last_name = name_split[1] email = user_data.get('email', '') + name = user_data['name'] + fullname, first_name, last_name = self.get_user_names(name) return {'username': name, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'email': email} diff --git a/social/backends/docker.py b/social/backends/docker.py index b91512a7a..ea73dbd6a 100644 --- a/social/backends/docker.py +++ b/social/backends/docker.py @@ -26,9 +26,14 @@ class DockerOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Docker.io account""" + fullname, first_name, last_name = self.get_user_names( + response.get('full_name') or response.get('username') or '' + ) return { 'username': response.get('username'), - 'first_name': response.get('full_name', response.get('username')), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'email': response.get('email', '') } diff --git a/social/backends/douban.py b/social/backends/douban.py index 3f274534a..f5f1ae613 100644 --- a/social/backends/douban.py +++ b/social/backends/douban.py @@ -42,8 +42,13 @@ class DoubanOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Douban""" + fullname, first_name, last_name = self.get_user_names( + response.get('name', '') + ) return {'username': response.get('uid', ''), - 'fullname': response.get('name', ''), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'email': ''} def user_data(self, access_token, *args, **kwargs): diff --git a/social/backends/dropbox.py b/social/backends/dropbox.py index 698636334..a4e4baf5f 100644 --- a/social/backends/dropbox.py +++ b/social/backends/dropbox.py @@ -22,9 +22,14 @@ class DropboxOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Dropbox account""" + fullname, first_name, last_name = self.get_user_names( + response.get('display_name') + ) return {'username': str(response.get('uid')), 'email': response.get('email'), - 'first_name': response.get('display_name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" @@ -45,9 +50,14 @@ class DropboxOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Dropbox account""" + fullname, first_name, last_name = self.get_user_names( + response.get('display_name') + ) return {'username': str(response.get('uid')), 'email': response.get('email'), - 'first_name': response.get('display_name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/facebook.py b/social/backends/facebook.py index c823c2624..01d8784dd 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -30,11 +30,16 @@ class FacebookOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Facebook account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name', ''), + response.get('first_name', ''), + response.get('last_name', '') + ) return {'username': response.get('username', response.get('name')), 'email': response.get('email', ''), - 'fullname': response.get('name', ''), - 'first_name': response.get('first_name', ''), - 'last_name': response.get('last_name', '')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/flickr.py b/social/backends/flickr.py index 56aa4f6e8..cae8975c3 100644 --- a/social/backends/flickr.py +++ b/social/backends/flickr.py @@ -19,9 +19,14 @@ class FlickrOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Flickr account""" + fullname, first_name, last_name = self.get_user_names( + response.get('fullname') + ) return {'username': response.get('username') or response.get('id'), 'email': '', - 'first_name': response.get('fullname')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/foursquare.py b/social/backends/foursquare.py index 84203720e..f6a5fba08 100644 --- a/social/backends/foursquare.py +++ b/social/backends/foursquare.py @@ -17,12 +17,16 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Foursquare account""" - firstName = response['response']['user']['firstName'] - lastName = response['response']['user'].get('lastName', '') - email = response['response']['user']['contact']['email'] - return {'username': firstName + ' ' + lastName, - 'first_name': firstName, - 'last_name': lastName, + info = response['response']['user'] + email = info['contact']['email'] + fullname, first_name, last_name = self.get_user_names( + first_name=info.get('firstName', ''), + last_name=info.get('lastName', '') + ) + return {'username': first_name + ' ' + last_name, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'email': email} def user_data(self, access_token, *args, **kwargs): diff --git a/social/backends/github.py b/social/backends/github.py index 8d452296b..b41c0a4a4 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -21,9 +21,14 @@ class GithubOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Github account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) return {'username': response.get('login'), 'email': response.get('email') or '', - 'first_name': response.get('name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" @@ -50,9 +55,14 @@ class GithubOrganizationOAuth2(GithubOAuth2): def get_user_details(self, response): """Return user details from Github account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) return {'username': response.get('login'), 'email': response.get('email') or '', - 'first_name': response.get('name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/google.py b/social/backends/google.py index a9582d180..e1a03574c 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -20,11 +20,17 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Orkut account""" email = response.get('email', '') + + fullname, first_name, last_name = self.get_user_names( + response.get('name', ''), + response.get('given_name', ''), + response.get('family_name', '') + ) return {'username': email.split('@', 1)[0], 'email': email, - 'fullname': response.get('name', ''), - 'first_name': response.get('given_name', ''), - 'last_name': response.get('family_name', '')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} class BaseGoogleOAuth2API(BaseGoogleAuth): diff --git a/social/backends/instagram.py b/social/backends/instagram.py index a6752ea4a..63eaf12ab 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -17,10 +17,14 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Instagram account""" username = response['user']['username'] - fullname = response['user'].get('full_name', '') email = response['user'].get('email', '') + fullname, first_name, last_name = self.get_user_names( + response['user'].get('full_name', '') + ) return {'username': username, - 'first_name': fullname, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, 'email': email} def user_data(self, access_token, *args, **kwargs): diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index 34243429a..572dd162e 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -19,10 +19,13 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Jawbone account""" data = response['data'] - first_name = data.get('first', '') - last_name = data.get('last', '') + fullname, first_name, last_name = self.get_user_names( + first_name=data.get('first', ''), + last_name=data.get('last', '') + ) return { 'username': first_name + ' ' + last_name, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'dob': data.get('dob', ''), diff --git a/social/backends/lastfm.py b/social/backends/lastfm.py index 565855089..c9ea837bc 100644 --- a/social/backends/lastfm.py +++ b/social/backends/lastfm.py @@ -47,10 +47,11 @@ def get_user_id(self, details, response): return response.get('name') def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names(response['name']) return { 'username': response['name'], 'email': '', - 'fullname': response['name'], - 'first_name': '', - 'last_name': '', + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } diff --git a/social/backends/legacy.py b/social/backends/legacy.py index 3e67f3d04..0ea474060 100644 --- a/social/backends/legacy.py +++ b/social/backends/legacy.py @@ -28,15 +28,11 @@ def get_user_details(self, response): """Return user details""" email = response.get('email', '') username = response.get('username', '') - fullname = response.get('fullname', '') - first_name = response.get('first_name', '') - last_name = response.get('last_name', '') - if fullname and not (first_name or last_name): - try: - first_name, last_name = fullname.split(' ', 1) - except ValueError: - first_name = fullname - last_name = last_name or '' + fullname, first_name, last_name = self.get_user_names( + response.get('fullname', ''), + response.get('first_name', ''), + response.get('last_name', '') + ) if email and not username: username = email.split('@', 1)[0] return { diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index 634820cad..8180a6679 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -15,11 +15,13 @@ class BaseLinkedinAuth(object): def get_user_details(self, response): """Return user details from Linkedin account""" - first_name = response['firstName'] - last_name = response['lastName'] + fullname, first_name, last_name = self.get_user_names( + first_name=response['firstName'], + last_name=response['lastName'] + ) email = response.get('emailAddress', '') return {'username': first_name + last_name, - 'fullname': first_name + ' ' + last_name, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'email': email} diff --git a/social/backends/live.py b/social/backends/live.py index 497daebe3..35af9203b 100644 --- a/social/backends/live.py +++ b/social/backends/live.py @@ -26,10 +26,15 @@ class LiveOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Live Connect account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('first_name'), + last_name=response.get('last_name') + ) return {'username': response.get('name'), 'email': response.get('emails', {}).get('account', ''), - 'first_name': response.get('first_name'), - 'last_name': response.get('last_name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/mailru.py b/social/backends/mailru.py index acce475af..7f9e17d4c 100644 --- a/social/backends/mailru.py +++ b/social/backends/mailru.py @@ -20,14 +20,15 @@ class MailruOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Mail.ru request""" - values = {'username': unquote(response['nick']), - 'email': unquote(response['email']), - 'first_name': unquote(response['first_name']), - 'last_name': unquote(response['last_name'])} - if values['first_name'] and values['last_name']: - values['fullname'] = ' '.join((values['first_name'], - values['last_name'])) - return values + fullname, first_name, last_name = self.get_user_names( + first_name=unquote(response['first_name']), + last_name=unquote(response['last_name']) + ) + return {'username': unquote(response['nick']), + 'email': unquote(response['email']), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Return user data from Mail.ru REST API""" diff --git a/social/backends/mixcloud.py b/social/backends/mixcloud.py index 9cf9f84e6..61e1605bf 100644 --- a/social/backends/mixcloud.py +++ b/social/backends/mixcloud.py @@ -13,11 +13,12 @@ class MixcloudOAuth2(BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': response['username'], 'email': None, - 'fullname': response['name'], - 'first_name': None, - 'last_name': None} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): return self.get_json('https://api.mixcloud.com/me/', diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 153db1362..f1422ee57 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -22,12 +22,17 @@ class OdnoklassnikiOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Odnoklassniki request""" + fullname, first_name, last_name = self.get_user_names( + fullname=unquote(response['name']), + first_name=unquote(response['first_name']), + last_name=unquote(response['last_name']) + ) return { 'username': response['uid'], 'email': '', - 'fullname': unquote(response['name']), - 'first_name': unquote(response['first_name']), - 'last_name': unquote(response['last_name']) + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } def user_data(self, access_token, *args, **kwargs): @@ -49,12 +54,17 @@ def extra_data(self, user, uid, response, details): if key in response['extra_data_list']]) def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names( + fullname=unquote(response['name']), + first_name=unquote(response['first_name']), + last_name=unquote(response['last_name']) + ) return { 'username': response['uid'], 'email': '', - 'fullname': unquote(response['name']), - 'first_name': unquote(response['first_name']), - 'last_name': unquote(response['last_name']) + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } def auth_complete(self, request, user, *args, **kwargs): diff --git a/social/backends/orkut.py b/social/backends/orkut.py index 00b4d6001..aa5053c14 100644 --- a/social/backends/orkut.py +++ b/social/backends/orkut.py @@ -16,11 +16,17 @@ def get_user_details(self, response): emails = response['emails'][0]['value'] except (KeyError, IndexError): emails = '' + + fullname, first_name, last_name = self.get_user_names( + fullname=response['displayName'], + first_name=response['name']['givenName'], + last_name=response['name']['familyName'] + ) return {'username': response['displayName'], 'email': emails, - 'fullname': response['displayName'], - 'first_name': response['name']['givenName'], - 'last_name': response['name']['familyName']} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from Orkut service""" diff --git a/social/backends/pixelpin.py b/social/backends/pixelpin.py index b2e224145..3a1ed2b3f 100644 --- a/social/backends/pixelpin.py +++ b/social/backends/pixelpin.py @@ -15,9 +15,15 @@ class PixelPinOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from PixelPin account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('firstName'), + last_name=response.get('lastName') + ) return {'username': response.get('firstName'), 'email': response.get('email') or '', - 'first_name': response.get('firstName')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/podio.py b/social/backends/podio.py index 46af3068c..1c4b0df44 100644 --- a/social/backends/podio.py +++ b/social/backends/podio.py @@ -22,8 +22,9 @@ def get_user_id(self, details, response): return response['ref']['id'] def get_user_details(self, response): - fullname = response['profile']['name'] - first_name, _, last_name = fullname.partition(' ') + fullname, first_name, last_name = self.get_user_names( + response['profile']['name'] + ) return { 'username': 'user_%d' % response['user']['user_id'], 'email': response['user']['mail'], diff --git a/social/backends/rdio.py b/social/backends/rdio.py index 51e475b7a..bce5fd648 100644 --- a/social/backends/rdio.py +++ b/social/backends/rdio.py @@ -12,11 +12,16 @@ class BaseRdio(OAuthAuth): ID_KEY = 'key' def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names( + fullname=response['displayName'], + first_name=response['firstName'], + last_name=response['lastName'] + ) return { 'username': response['username'], - 'first_name': response['firstName'], - 'last_name': response['lastName'], - 'fullname': response['displayName'], + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } diff --git a/social/backends/readability.py b/social/backends/readability.py index 72f7f785a..76e241f15 100644 --- a/social/backends/readability.py +++ b/social/backends/readability.py @@ -21,9 +21,14 @@ class ReadabilityOAuth(BaseOAuth1): ('email_into_address', 'email_into_address')] def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names( + first_name=response['first_name'], + last_name=response['last_name'] + ) return {'username': response['username'], - 'first_name': response['first_name'], - 'last_name': response['last_name']} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token): return self.get_json(READABILITY_API + '/users/_current', diff --git a/social/backends/runkeeper.py b/social/backends/runkeeper.py index 2ecb0a06a..d7a2f3bf5 100644 --- a/social/backends/runkeeper.py +++ b/social/backends/runkeeper.py @@ -26,9 +26,14 @@ def get_user_details(self, response): profile_url_parts = profile_url.split('http://runkeeper.com/user/') if len(profile_url_parts) > 1 and len(profile_url_parts[1]): username = profile_url_parts[1] + fullname, first_name, last_name = self.get_user_names( + fullname=response.get('name') + ) return {'username': username, 'email': response.get('email') or '', - 'first_name': response.get('name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): # We need to use the /user endpoint to get the user id, the /profile diff --git a/social/backends/skyrock.py b/social/backends/skyrock.py index 3492e3155..b027d94ac 100644 --- a/social/backends/skyrock.py +++ b/social/backends/skyrock.py @@ -16,11 +16,15 @@ class SkyrockOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Skyrock account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response['firstname'], + last_name=response['name'] + ) return {'username': response['username'], 'email': response['email'], - 'fullname': response['firstname'] + ' ' + response['name'], - 'first_name': response['firstname'], - 'last_name': response['name']} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token): """Return user data provided""" diff --git a/social/backends/soundcloud.py b/social/backends/soundcloud.py index 2c43234cd..ab41abd44 100644 --- a/social/backends/soundcloud.py +++ b/social/backends/soundcloud.py @@ -22,12 +22,9 @@ class SoundcloudOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Soundcloud account""" - fullname = response.get('full_name') - full_name = fullname.split(' ') - first_name = full_name[0] - last_name = '' - if len(full_name) > 1: - last_name = full_name[-1] + fullname, first_name, last_name = self.get_user_names( + response.get('full_name') + ) return {'username': response.get('username'), 'email': response.get('email') or '', 'fullname': fullname, diff --git a/social/backends/stackoverflow.py b/social/backends/stackoverflow.py index 5ddd0d103..1da1fa9ca 100644 --- a/social/backends/stackoverflow.py +++ b/social/backends/stackoverflow.py @@ -20,8 +20,13 @@ class StackoverflowOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from Stackoverflow account""" + fullname, first_name, last_name = self.get_user_names( + response.get('display_name') + ) return {'username': response.get('link').rsplit('/', 1)[-1], - 'full_name': response.get('display_name')} + 'full_name': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/stocktwits.py b/social/backends/stocktwits.py index 601272f31..55fee95ab 100644 --- a/social/backends/stocktwits.py +++ b/social/backends/stocktwits.py @@ -20,14 +20,12 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Stocktwits account""" - try: - first_name, last_name = response['user']['name'].split(' ', 1) - except: - first_name = response['user']['name'] - last_name = '' + fullname, first_name, last_name = self.get_user_names( + response['user']['name'] + ) return {'username': response['user']['username'], 'email': '', # not supplied - 'fullname': response['user']['name'], + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} diff --git a/social/backends/strava.py b/social/backends/strava.py index e80c8a632..1c903d21f 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -18,10 +18,14 @@ def get_user_details(self, response): """Return user details from Strava account""" # because there is no usernames on strava username = response['athlete']['id'] - first_name = response['athlete'].get('first_name', '') email = response['athlete'].get('email', '') + fullname, first_name, last_name = self.get_user_names( + first_name=response['athlete'].get('first_name', '') + ) return {'username': str(username), + 'fullname': fullname, 'first_name': first_name, + 'last_name': last_name, 'email': email} def user_data(self, access_token, *args, **kwargs): diff --git a/social/backends/thisismyjam.py b/social/backends/thisismyjam.py index 9ee5094f8..994691f7d 100644 --- a/social/backends/thisismyjam.py +++ b/social/backends/thisismyjam.py @@ -15,12 +15,16 @@ class ThisIsMyJamOAuth1(BaseOAuth1): def get_user_details(self, response): """Return user details from ThisIsMyJam account""" + info = response.get('person') + fullname, first_name, last_name = self.get_user_names( + info.get('fullname') + ) return { - 'username': response.get('person').get('name'), - 'fullname': response.get('person').get('fullname'), + 'username': info.get('name'), 'email': '', - 'first_name': '', - 'last_name': '' + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } def user_data(self, access_token, *args, **kwargs): diff --git a/social/backends/trello.py b/social/backends/trello.py index cc069acf2..73eaa916d 100644 --- a/social/backends/trello.py +++ b/social/backends/trello.py @@ -22,9 +22,14 @@ class TrelloOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Trello account""" + fullname, first_name, last_name = self.get_user_names( + response.get('fullName') + ) return {'username': response.get('username'), 'email': response.get('email'), - 'fullName': response.get('fullName')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token): """Return user data provided""" diff --git a/social/backends/tripit.py b/social/backends/tripit.py index 566400d00..ac09334be 100644 --- a/social/backends/tripit.py +++ b/social/backends/tripit.py @@ -17,14 +17,10 @@ class TripItOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from TripIt account""" - try: - first_name, last_name = response['name'].split(' ', 1) - except ValueError: - first_name = response['name'] - last_name = '' + fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': response['screen_name'], 'email': response['email'], - 'fullname': response['name'], + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} diff --git a/social/backends/twitter.py b/social/backends/twitter.py index ca351f813..c096a3390 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -22,14 +22,10 @@ def process_error(self, data): def get_user_details(self, response): """Return user details from Twitter account""" - try: - first_name, last_name = response['name'].split(' ', 1) - except: - first_name = response['name'] - last_name = '' + fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': response['screen_name'], 'email': '', # not supplied - 'fullname': response['name'], + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} diff --git a/social/backends/vimeo.py b/social/backends/vimeo.py index f08b637e9..85d236f6a 100644 --- a/social/backends/vimeo.py +++ b/social/backends/vimeo.py @@ -14,11 +14,9 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Twitter account""" person = response.get('person', {}) - fullname = person.get('display_name', '') - if ' ' in fullname: - first_name, last_name = fullname.split(' ', 1) - else: - first_name, last_name = fullname, '' + fullname, first_name, last_name = self.get_user_names( + person.get('display_name', '') + ) return {'username': person.get('username', ''), 'email': '', 'fullname': fullname, @@ -38,12 +36,11 @@ class VimeoOAuth2(BaseOAuth2): """Vimeo OAuth2 authentication backend""" name = 'vimeo-oauth2' AUTHORIZATION_URL = 'https://api.vimeo.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api.vimeo.com/oauth/access_token' + ACCESS_TOKEN_URL = 'https://api.vimeo.com/oauth/access_token' REFRESH_TOKEN_URL = 'https://api.vimeo.com/oauth/request_token' ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' - - API_ACCEPT_HEADER = {'Accept' : 'application/vnd.vimeo.*+json;version=3.0'} + API_ACCEPT_HEADER = {'Accept': 'application/vnd.vimeo.*+json;version=3.0'} def get_redirect_uri(self, state=None): """ @@ -65,23 +62,18 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from account""" user = response.get('user', {}) - fullname = user.get('name', '') - - if ' ' in fullname: - first_name, last_name = fullname.split(' ', 1) - else: - first_name, last_name = fullname, '' - + fullname, first_name, last_name = self.get_user_names( + user.get('name', '') + ) return {'username': fullname, 'fullname': fullname, 'first_name': first_name, - 'last_name': last_name,} + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( 'https://api.vimeo.com/me', - params={'access_token' : access_token}, + params={'access_token': access_token}, headers=VimeoOAuth2.API_ACCEPT_HEADER, ) - diff --git a/social/backends/vk.py b/social/backends/vk.py index 526c12cb4..d27e29a88 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -20,14 +20,16 @@ class VKontakteOpenAPI(BaseAuth): def get_user_details(self, response): """Return user details from VK.com request""" nickname = response.get('nickname') or '' + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('first_name', [''])[0], + last_name=response.get('last_name', [''])[0] + ) return { 'username': response['id'] if len(nickname) == 0 else nickname, 'email': '', - 'fullname': '', - 'first_name': response.get('first_name')[0] - if 'first_name' in response else '', - 'last_name': response.get('last_name')[0] - if 'last_name' in response else '' + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } def user_data(self, access_token, *args, **kwargs): @@ -85,10 +87,15 @@ class VKOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from VK.com account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('first_name'), + last_name=response.get('last_name') + ) return {'username': response.get('screen_name'), 'email': '', - 'first_name': response.get('first_name'), - 'last_name': response.get('last_name')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, response, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/weibo.py b/social/backends/weibo.py index 1e287c9ef..b779520fb 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -31,8 +31,13 @@ def get_user_details(self, response): username = response.get('domain', '') else: username = response.get('name', '') + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('screen_name', '') + ) return {'username': username, - 'first_name': response.get('screen_name', '')} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): return self.get_json('https://api.weibo.com/2/users/show.json', diff --git a/social/backends/xing.py b/social/backends/xing.py index 574690190..915514207 100644 --- a/social/backends/xing.py +++ b/social/backends/xing.py @@ -19,10 +19,13 @@ class XingOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Xing account""" - first_name, last_name = response['first_name'], response['last_name'] email = response.get('email', '') + fullname, first_name, last_name = self.get_user_names( + first_name=response['first_name'], + last_name=response['last_name'] + ) return {'username': first_name + last_name, - 'fullname': first_name + ' ' + last_name, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'email': email} diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index c1590a527..746b9b37f 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -28,16 +28,18 @@ class YahooOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Yahoo Profile""" - fname = response.get('givenName') - lname = response.get('familyName') + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('givenName'), + last_name=response.get('familyName') + ) emails = [email for email in response.get('emails', []) if email.get('handle')] emails.sort(key=lambda e: e.get('primary', False)) return {'username': response.get('nickname'), 'email': emails[0]['handle'] if emails else '', - 'fullname': '{0} {1}'.format(fname, lname), - 'first_name': fname, - 'last_name': lname} + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/yammer.py b/social/backends/yammer.py index adcdde6f2..f52e28cf0 100644 --- a/social/backends/yammer.py +++ b/social/backends/yammer.py @@ -20,15 +20,17 @@ def get_user_id(self, details, response): def get_user_details(self, response): username = response['user']['name'] - first_name = response['user']['first_name'] - last_name = response['user']['last_name'] - full_name = response['user']['full_name'] + fullname, first_name, last_name = self.get_user_names( + fullname=response['user']['full_name'], + first_name=response['user']['first_name'], + last_name=response['user']['last_name'] + ) email = response['user']['contact']['email_addresses'][0]['address'] mugshot_url = response['user']['mugshot_url'] return { 'username': username, 'email': email, - 'fullname': full_name, + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'picture_url': mugshot_url diff --git a/social/backends/yandex.py b/social/backends/yandex.py index 89cf3b5cb..144ee5cd7 100644 --- a/social/backends/yandex.py +++ b/social/backends/yandex.py @@ -38,13 +38,13 @@ class YandexOAuth2(BaseOAuth2): REDIRECT_STATE = False def get_user_details(self, response): - first_name = response.get('real_name') or response.get('display_name') - last_name = '' - if ' ' in first_name: - first_name, last_name = first_name.split(' ', 1) + fullname, first_name, last_name = self.get_user_names( + response.get('real_name') or response.get('display_name') or '' + ) return {'username': response.get('display_name'), 'email': response.get('default_email') or response.get('emails', [''])[0], + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} @@ -62,13 +62,13 @@ class YaruOAuth2(BaseOAuth2): REDIRECT_STATE = False def get_user_details(self, response): - first_name = response.get('real_name') or response.get('display_name') - last_name = '' - if ' ' in first_name: - first_name, last_name = first_name.split(' ', 1) + fullname, first_name, last_name = self.get_user_names( + response.get('real_name') or response.get('display_name') or '' + ) return {'username': response.get('display_name'), 'email': response.get('default_email') or response.get('emails', [''])[0], + 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} From 4d6bd7b6fc28ffb0c77d0d84ad36727604a059e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 23 Apr 2014 23:37:19 -0300 Subject: [PATCH 210/890] Allow overrideable values for AX schema attrs and SReg attributes in OpenId. Fixes #258 --- social/backends/open_id.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 464bdb1fb..c9e57cbb6 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -42,6 +42,14 @@ def get_user_id(self, details, response): """Return user unique id provided by service""" return response.identity_url + def get_ax_attributes(self): + return self.setting('AX_SCHEMA_ATTRS') or ( + AX_SCHEMA_ATTRS + OLD_AX_ATTRS + ) + + def get_sreg_attributes(self): + return self.setting('SREG_ATTR') or SREG_ATTR + def values_from_response(self, response, sreg_names=None, ax_names=None): """Return values from SimpleRegistration response or AttributeExchange response if present. @@ -73,10 +81,9 @@ def get_user_details(self, response): 'first_name': '', 'last_name': ''} # update values using SimpleRegistration or AttributeExchange # values - values.update(self.values_from_response(response, - SREG_ATTR, - OLD_AX_ATTRS + - AX_SCHEMA_ATTRS)) + values.update(self.values_from_response( + response, self.get_sreg_attributes(), self.get_ax_attributes() + )) fullname = values.get('fullname') or '' first_name = values.get('first_name') or '' @@ -174,12 +181,12 @@ def setup_request(self, params=None): if request.endpoint.supportsType(ax.AXMessage.ns_uri): fetch_request = ax.FetchRequest() # Mark all attributes as required, Google ignores optional ones - for attr, alias in (AX_SCHEMA_ATTRS + OLD_AX_ATTRS): + for attr, alias in self.get_ax_attributes(): fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True)) else: fetch_request = sreg.SRegRequest( - optional=list(dict(SREG_ATTR).keys()) + optional=list(dict(self.get_sreg_attributes()).keys()) ) request.addExtension(fetch_request) From a65b385769d33b606ea4b7c11c5542ea7d9394b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 23 Apr 2014 23:43:52 -0300 Subject: [PATCH 211/890] Disable redirect_state in strava backend. Fixes #259 --- social/backends/strava.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/social/backends/strava.py b/social/backends/strava.py index 1c903d21f..b59be7387 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -10,6 +10,11 @@ class StravaOAuth(BaseOAuth2): AUTHORIZATION_URL = 'https://www.strava.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://www.strava.com/oauth/token' ACCESS_TOKEN_METHOD = 'POST' + # Strava doesn't check for parameters in redirect_uri and directly appends + # the auth parameters to it, ending with an URL like: + # http://example.com/complete/strava?redirect_state=xxx?code=xxx&state=xxx + # Check issue #259 for details. + REDIRECT_STATE = False def get_user_id(self, details, response): return response['athlete']['id'] From 2451a51c3c3439154ac782a049ee7ffa877c12e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 30 Apr 2014 01:53:36 -0300 Subject: [PATCH 212/890] Update amazon docs, drop outdate details about bug. Fixes #260 --- docs/backends/amazon.rst | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/docs/backends/amazon.rst b/docs/backends/amazon.rst index d279c3263..7c2396ff1 100644 --- a/docs/backends/amazon.rst +++ b/docs/backends/amazon.rst @@ -19,38 +19,6 @@ enable ``python-social-auth`` support follow this steps: ... ) - -Notes ------ - -At the moment (May 29, 2013), Amazon API doesn't work properly, for example -users are being redirected to URLs like:: - - https://www.amazon.com:80/ap/signin?... - -Which are invalid (``https`` over port 80?). The process works OK when removing -the ``:80`` from those URLs, but this renders the service very unusable at the -moment. - -User data returned by Amazon doesn't follow the documented format:: - - { - Request-Id: "02GGTU7CWMNFTV3KH3J6", - Profile: { - Name: "Foo Bar", - CustomerId: "amzn1.account.ABCDE1234", - PrimaryEmail: "foo@bar.com" - } - } - -Instead of:: - - { - "user_id": "amzn1.account.ABCDE1234", - "email": "foo@bar.com", - "name": "Foo Bar" - } - Further documentation at `Website Developer Guide`_ and `Getting Started for Web`_. .. _Amazon App Console: http://login.amazon.com/manageApps From f5ae2894d1ced2c800b89f93705f166e7c79fe89 Mon Sep 17 00:00:00 2001 From: momamene Date: Wed, 30 Apr 2014 20:27:13 +0900 Subject: [PATCH 213/890] Add Kakao backend --- docs/backends/index.rst | 1 + docs/backends/kakao.rst | 17 +++++++++++++++ docs/intro.rst | 2 ++ social/backends/kakao.py | 34 +++++++++++++++++++++++++++++ social/tests/backends/test_kakao.py | 26 ++++++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 docs/backends/kakao.rst create mode 100644 social/backends/kakao.py create mode 100644 social/tests/backends/test_kakao.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 8a43ac91f..4f2bb21f0 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -71,6 +71,7 @@ Social backends google instagram jawbone + kakao lastfm linkedin livejournal diff --git a/docs/backends/kakao.rst b/docs/backends/kakao.rst new file mode 100644 index 000000000..8d78ff141 --- /dev/null +++ b/docs/backends/kakao.rst @@ -0,0 +1,17 @@ +Kakao +====== + +Kakao uses OAuth v2 for Authentication. + +- Register a new applicationat the `Kakao API`_, and + +- Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_KAKAO_KEY = '' + SOCIAL_AUTH_KAKAO_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_KAKAO_SCOPE = [...] + +.. _Kakao API: https://developers.kakao.com/ diff --git a/docs/intro.rst b/docs/intro.rst index b1335e8eb..d43bb7cb4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -51,6 +51,7 @@ or extend current one): * Github_ OAuth2 * Google_ OAuth1, OAuth2 and OpenId * Instagram_ OAuth2 + * Kakao_ OAuth2 * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId @@ -126,6 +127,7 @@ section. .. _Github: https://github.com .. _Google: http://google.com .. _Instagram: https://instagram.com +.. _Kakao: https://kakao.com .. _Linkedin: https://www.linkedin.com .. _Live: https://www.live.com .. _Livejournal: http://livejournal.com diff --git a/social/backends/kakao.py b/social/backends/kakao.py new file mode 100644 index 000000000..014934f20 --- /dev/null +++ b/social/backends/kakao.py @@ -0,0 +1,34 @@ +""" +Kakao OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/kakao.html +""" +from social.backends.oauth import BaseOAuth2 + + +class KakaoOAuth2(BaseOAuth2): + """Kakao OAuth authentication backend""" + name = 'kakao' + AUTHORIZATION_URL = 'https://kauth.kakao.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://kauth.kakao.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + + def get_user_id(self, details, response): + return response['id'] + + def get_user_details(self, response): + """Return user details from Kakao account""" + nickname = response['properties']['nickname'] + thumbnail_image = response['properties']['thumbnail_image'] + profile_image = response['properties']['profile_image'] + return { + 'username': nickname, + 'email': '', + 'fullname': '', + 'first_name': '', + 'last_name': '' + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://kapi.kakao.com/v1/user/me', + params={'access_token': access_token}) diff --git a/social/tests/backends/test_kakao.py b/social/tests/backends/test_kakao.py new file mode 100644 index 000000000..c8abb6b51 --- /dev/null +++ b/social/tests/backends/test_kakao.py @@ -0,0 +1,26 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class KakaoOAuth2Test(OAuth2Test): + backend_path = 'social.backends.kakao.KakaoOAuth2' + user_data_url = 'https://kapi.kakao.com/v1/user/me' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar' + }) + user_data_body = json.dumps({ + 'id': '101010101', + 'properties': { + 'nickname': 'foobar', + 'thumbnail_image': 'http://mud-kage.kakao.co.kr/14/dn/btqbh1AKmRf/ujlHpQhxtMSbhKrBisrxe1/o.jpg', + 'profile_image': 'http://mud-kage.kakao.co.kr/14/dn/btqbjCnl06Q/wbMJSVAUZB7lzSImgGdsoK/o.jpg' + } + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From f65110cefeb980e8a90f7bdde15fe26415e927ad Mon Sep 17 00:00:00 2001 From: Kyle Richelhoff Date: Thu, 1 May 2014 10:52:08 -0600 Subject: [PATCH 214/890] Added LoginRadius backend. --- README.rst | 1 + docs/backends/index.rst | 1 + docs/backends/loginradius.rst | 17 ++++++ social/backends/loginradius.py | 101 +++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 docs/backends/loginradius.rst create mode 100644 social/backends/loginradius.py diff --git a/README.rst b/README.rst index 35a5106b6..16381d902 100644 --- a/README.rst +++ b/README.rst @@ -76,6 +76,7 @@ or current ones extended): * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId + * LoginRadius_ OAuth2 and Application Auth * Mailru_ OAuth2 * Mendeley_ OAuth1 http://mendeley.com * Mixcloud_ OAuth2 diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 4f2bb21f0..fc011f004 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -76,6 +76,7 @@ Social backends linkedin livejournal live + loginradius mailru mendeley mixcloud diff --git a/docs/backends/loginradius.rst b/docs/backends/loginradius.rst new file mode 100644 index 000000000..c5464ec4f --- /dev/null +++ b/docs/backends/loginradius.rst @@ -0,0 +1,17 @@ +LoginRadius +=========== + +LoginRadius uses OAuth v2 and BaseAuth for Authentication with other providers. + +- Register a new application at the `LoginRadius Website`_, and + +- Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_LOGINRADIUS_KEY = '' + SOCIAL_AUTH_LOGINRADIUS_SECRET = '' + +- Further documentation can be found at `LoginRadius API Documentation`_ and `LoginRadius Datapoints`_ + +.. _LoginRadius Website: https://loginradius.com/ +.. _LoginRadius API Documentation: http://api.loginradius.com/help/ +.. _LoginRadius Datapoints: http://www.loginradius.com/datapoints/ \ No newline at end of file diff --git a/social/backends/loginradius.py b/social/backends/loginradius.py new file mode 100644 index 000000000..b32bc5ac3 --- /dev/null +++ b/social/backends/loginradius.py @@ -0,0 +1,101 @@ +""" +LoginRadius BaseAuth/BaseOAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/loginradius.html +""" +from social.backends.oauth import BaseAuth, BaseOAuth2 +from social.exceptions import AuthCanceled, AuthUnknownError +from requests import HTTPError + + +class LoginRadiusAuth(BaseOAuth2, BaseAuth): + """LoginRadius BaseAuth/BaseOAuth2 authentication backend.""" + name = 'loginradius' + ID_KEY = 'ID' + ACCESS_TOKEN_URL = 'https://api.loginradius.com/api/v2/access_token' + PROFILE_URL = 'https://api.loginradius.com/api/v2/userprofile' + ACCESS_TOKEN_METHOD = 'GET' + REDIRECT_STATE = False + STATE_PARAMETER = False + DEFAULT_SCOPE = None + + def uses_redirect(self): + """Return False because we return HTML instead.""" + return self.REDIRECT_STATE + + def auth_html(self): + """Must return login HTML content returned by provider.""" + login_script = """ +
          + + " + return login_script + + def auth_headers(self): + """Static headers.""" + return {'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json'} + + def auth_complete(self, *args, **kwargs): + """Completes logging process, must return user instance.""" + self.process_error(self.data) + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + params={'token': self.data.get("token"), 'secret': self.setting('SECRET')}, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + raise + except KeyError: + raise AuthUnknownError(self) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service. Implement in subclass.""" + profile_data = self.get_json( + self.PROFILE_URL, + params={'access_token': access_token}, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + return profile_data + + def get_user_details(self, response): + """Must return user details in a know internal struct: + {'username': , + 'email': , + 'fullname': , + 'first_name': , + 'last_name': } + """ + profile = { + 'username': response['NickName'] or '', + 'email': response['Email'][0]['Value'] or '', + 'fullname': response['FullName'] or '', + 'first_name': response['FirstName'] or '', + 'last_name': response['LastName'] or '' + } + return profile + + def get_user_id(self, details, response): + """Return a unique ID for the current user, by default from server + response. + + Since LoginRadius handles multiple providers, we need to distinguish them to prevent conflicts. + """ + return str(response.get("Provider") + "-" + response.get(self.ID_KEY)) \ No newline at end of file From 51e3a088ba0fa559e17a5d1e47cd62672ed38896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 1 May 2014 18:51:20 -0300 Subject: [PATCH 215/890] PEP8 and logic simplification --- docs/backends/loginradius.rst | 37 +++++++++++++++-- social/backends/loginradius.py | 76 ++++++++++------------------------ 2 files changed, 56 insertions(+), 57 deletions(-) diff --git a/docs/backends/loginradius.rst b/docs/backends/loginradius.rst index c5464ec4f..f16d57689 100644 --- a/docs/backends/loginradius.rst +++ b/docs/backends/loginradius.rst @@ -1,7 +1,8 @@ LoginRadius =========== -LoginRadius uses OAuth v2 and BaseAuth for Authentication with other providers. +LoginRadius uses OAuth2 for Authentication with other providers with an HTML +widget used to trigger the auth process. - Register a new application at the `LoginRadius Website`_, and @@ -10,8 +11,38 @@ LoginRadius uses OAuth v2 and BaseAuth for Authentication with other providers. SOCIAL_AUTH_LOGINRADIUS_KEY = '' SOCIAL_AUTH_LOGINRADIUS_SECRET = '' -- Further documentation can be found at `LoginRadius API Documentation`_ and `LoginRadius Datapoints`_ +- Since the auth process is triggered by LoginRadius JS script, you need to + sever such content to the user, all you need to do that is a template with + the following content:: + +
          + + + + Put that content in a template named ``loginradius.html`` (accessible to your + framework), or define a name with ``SOCIAL_AUTH_LOGINRADIUS_TEMPLATE`` setting, + like:: + + SOCIAL_AUTH_LOGINRADIUS_LOCAL_HTML = 'loginradius.html' + + The template context will have the current backend instance under the + ``backend`` name, also the application key (``LOGINRADIUS_KEY``) and the + redirect URL (``LOGINRADIUS_REDIRECT_URL``). + +- Further documentation can be found at `LoginRadius API Documentation`_ and + `LoginRadius Datapoints`_ .. _LoginRadius Website: https://loginradius.com/ .. _LoginRadius API Documentation: http://api.loginradius.com/help/ -.. _LoginRadius Datapoints: http://www.loginradius.com/datapoints/ \ No newline at end of file +.. _LoginRadius Datapoints: http://www.loginradius.com/datapoints/ diff --git a/social/backends/loginradius.py b/social/backends/loginradius.py index b32bc5ac3..e7193b4cc 100644 --- a/social/backends/loginradius.py +++ b/social/backends/loginradius.py @@ -1,14 +1,12 @@ """ -LoginRadius BaseAuth/BaseOAuth2 backend, docs at: +LoginRadius BaseOAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/loginradius.html """ -from social.backends.oauth import BaseAuth, BaseOAuth2 -from social.exceptions import AuthCanceled, AuthUnknownError -from requests import HTTPError +from social.backends.oauth import BaseOAuth2 -class LoginRadiusAuth(BaseOAuth2, BaseAuth): - """LoginRadius BaseAuth/BaseOAuth2 authentication backend.""" +class LoginRadiusAuth(BaseOAuth2): + """LoginRadius BaseOAuth2 authentication backend.""" name = 'loginradius' ID_KEY = 'ID' ACCESS_TOKEN_URL = 'https://api.loginradius.com/api/v2/access_token' @@ -16,64 +14,35 @@ class LoginRadiusAuth(BaseOAuth2, BaseAuth): ACCESS_TOKEN_METHOD = 'GET' REDIRECT_STATE = False STATE_PARAMETER = False - DEFAULT_SCOPE = None def uses_redirect(self): """Return False because we return HTML instead.""" - return self.REDIRECT_STATE + return False def auth_html(self): - """Must return login HTML content returned by provider.""" - login_script = """ -
          - - " - return login_script - - def auth_headers(self): - """Static headers.""" - return {'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json'} + key, secret = self.get_key_and_secret() + tpl = self.setting('TEMPLATE', 'loginradius.html') + return self.strategy.render_html(tpl=tpl, context={ + 'backend': self, + 'LOGINRADIUS_KEY': key, + 'LOGINRADIUS_REDIRECT_URL': self.get_redirect_uri() + }) - def auth_complete(self, *args, **kwargs): - """Completes logging process, must return user instance.""" - self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - params={'token': self.data.get("token"), 'secret': self.setting('SECRET')}, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) + def request_access_token(self, *args, **kwargs): + return self.get_json(params={ + 'token': self.data.get('token'), + 'secret': self.setting('SECRET') + }, *args, **kwargs) def user_data(self, access_token, *args, **kwargs): """Loads user data from service. Implement in subclass.""" - profile_data = self.get_json( + return self.get_json( self.PROFILE_URL, params={'access_token': access_token}, data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD ) - return profile_data def get_user_details(self, response): """Must return user details in a know internal struct: @@ -94,8 +63,7 @@ def get_user_details(self, response): def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server - response. - - Since LoginRadius handles multiple providers, we need to distinguish them to prevent conflicts. - """ - return str(response.get("Provider") + "-" + response.get(self.ID_KEY)) \ No newline at end of file + response. Since LoginRadius handles multiple providers, we need to + distinguish them to prevent conflicts.""" + return '{0}-{1}'.format(response.get('Provider'), + response.get(self.ID_KEY)) From 19a1e8e58f24a881fb8333a234c09a38d3390d99 Mon Sep 17 00:00:00 2001 From: Daniel Ryan Date: Tue, 6 May 2014 15:55:36 -0400 Subject: [PATCH 216/890] added new backend classes for Facebook that use the Open Graph 2.0 endpoints --- social/backends/facebook.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 01d8784dd..fed2eaca2 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -179,3 +179,20 @@ def base64_url_decode(data): if constant_time_compare(sig, expected_sig) and \ data['issued_at'] > (time.time() - 86400): return data + +class Facebook2OAuth2(FacebookOAuth2): + """Facebook OAuth2 authentication backend using Facebook Open Graph 2.0""" + + AUTHORIZATION_URL = 'https://www.facebook.com/v2.0/dialog/oauth' + ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.0/oauth/access_token' + REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.0/{uid}/permissions' + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + params = self.setting('PROFILE_EXTRA_PARAMS', {}) + params['access_token'] = access_token + return self.get_json('https://graph.facebook.com/v2.0/me', + params=params) + +class Facebook2AppOAuth2(Facebook2OAuth2, FacebookAppOAuth2): + pass From bec5ff8ae08b3dfa13912dbde762884e46f25ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 6 May 2014 21:10:05 -0300 Subject: [PATCH 217/890] Settings to override default scope/attrs and docs about them. Refs #258 --- docs/configuration/settings.rst | 18 ++++++++++++++++++ social/backends/oauth.py | 6 ++++-- social/backends/open_id.py | 7 ++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 6b9bf2222..27474f27f 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -182,6 +182,24 @@ settings in the same way explained above but with this other suffix:: _REQUEST_TOKEN_EXTRA_ARGUMENTS = {...} +Basic information is requested to the different providers in order to create +a coherent user instance (with first and last name, email and full name), this +could be too intrusive for some sites that want to ask users the minimum data +possible. It's possible to override the default values requested by defining +any of the following settings, for Open Id providers:: + + SOCIAL_AUTH__IGNORE_DEFAULT_AX_ATTRS = True + SOCIAL_AUTH__AX_SCHEMA_ATTRS = [ + (schema, alias) + ] + +For OAuth backends:: + + SOCIAL_AUTH__IGNORE_DEFAULT_SCOPE = True + SOCIAL_AUTH__SCOPE = [ + ... + ] + Processing redirects and urlopen -------------------------------- diff --git a/social/backends/oauth.py b/social/backends/oauth.py index d5b04fb7b..3f091bf7a 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -38,8 +38,10 @@ def extra_data(self, user, uid, response, details=None): def get_scope(self): """Return list with needed access scope""" - return (self.DEFAULT_SCOPE or []) + \ - self.setting('SCOPE', []) + scope = self.setting('SCOPE', []) + if not self.setting('IGNORE_DEFAULT_SCOPE', False): + scope += self.DEFAULT_SCOPE or [] + return scope def get_scope_argument(self): param = {} diff --git a/social/backends/open_id.py b/social/backends/open_id.py index c9e57cbb6..7db67d05e 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -43,9 +43,10 @@ def get_user_id(self, details, response): return response.identity_url def get_ax_attributes(self): - return self.setting('AX_SCHEMA_ATTRS') or ( - AX_SCHEMA_ATTRS + OLD_AX_ATTRS - ) + attrs = self.setting('AX_SCHEMA_ATTRS', []) + if attrs and self.setting('IGNORE_DEFAULT_AX_ATTRS', True): + return attrs + return attrs + AX_SCHEMA_ATTRS + OLD_AX_ATTRS def get_sreg_attributes(self): return self.setting('SREG_ATTR') or SREG_ATTR From 2e9f86f7103c9251573c67b0168ea896a37cc120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 6 May 2014 21:18:37 -0300 Subject: [PATCH 218/890] PEP8 and docs about Facebook Graph 2.0 backends --- docs/backends/facebook.rst | 8 ++++++++ social/backends/facebook.py | 13 ++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/backends/facebook.rst b/docs/backends/facebook.rst index 04991f372..67c3ea1d9 100644 --- a/docs/backends/facebook.rst +++ b/docs/backends/facebook.rst @@ -74,6 +74,14 @@ If you need to perform authentication from Facebook Canvas application: More info on the topic at `Facebook Canvas Application Authentication`_. + +Graph 2.0 +--------- + +If looking for `Graph 2.0`_ support, use the backends ``Facebook2OAuth2`` +(OAuth2) and/or ``Facebook2AppOAuth2`` (Canvas application). + .. _Facebook development resources: http://developers.facebook.com/docs/authentication/ .. _Facebook App Creation: http://developers.facebook.com/setup/ .. _Facebook Canvas Application Authentication: http://www.ikrvss.ru/2011/09/22/django-social-auth-and-facebook-canvas-applications/ +.. _Graph 2.0: https://developers.facebook.com/blog/post/2014/04/30/the-new-facebook-login/ diff --git a/social/backends/facebook.py b/social/backends/facebook.py index fed2eaca2..0da7223e9 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -23,6 +23,7 @@ class FacebookOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://graph.facebook.com/oauth/access_token' REVOKE_TOKEN_URL = 'https://graph.facebook.com/{uid}/permissions' REVOKE_TOKEN_METHOD = 'DELETE' + USER_DATA_URL = 'https://graph.facebook.com/me' EXTRA_DATA = [ ('id', 'id'), ('expires', 'expires') @@ -45,8 +46,7 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" params = self.setting('PROFILE_EXTRA_PARAMS', {}) params['access_token'] = access_token - return self.get_json('https://graph.facebook.com/me', - params=params) + return self.get_json(self.USER_DATA_URL, params=params) def process_error(self, data): super(FacebookOAuth2, self).process_error(data) @@ -180,19 +180,14 @@ def base64_url_decode(data): data['issued_at'] > (time.time() - 86400): return data + class Facebook2OAuth2(FacebookOAuth2): """Facebook OAuth2 authentication backend using Facebook Open Graph 2.0""" - AUTHORIZATION_URL = 'https://www.facebook.com/v2.0/dialog/oauth' ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.0/oauth/access_token' REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.0/{uid}/permissions' + USER_DATA_URL = 'https://graph.facebook.com/v2.0/me' - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - params = self.setting('PROFILE_EXTRA_PARAMS', {}) - params['access_token'] = access_token - return self.get_json('https://graph.facebook.com/v2.0/me', - params=params) class Facebook2AppOAuth2(Facebook2OAuth2, FacebookAppOAuth2): pass From d393fa6c0452dfc7beb4da7981fe0268e94d5f05 Mon Sep 17 00:00:00 2001 From: Marno Krahmer Date: Wed, 7 May 2014 15:41:13 +0200 Subject: [PATCH 219/890] Change the authorization url for the xing api --- social/backends/xing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/xing.py b/social/backends/xing.py index 915514207..1a0381d4a 100644 --- a/social/backends/xing.py +++ b/social/backends/xing.py @@ -8,7 +8,7 @@ class XingOAuth(BaseOAuth1): """Xing OAuth authentication backend""" name = 'xing' - AUTHORIZATION_URL = 'https://www.xing.com/v1/authorize' + AUTHORIZATION_URL = 'https://api.xing.com/v1/authorize' REQUEST_TOKEN_URL = 'https://api.xing.com/v1/request_token' ACCESS_TOKEN_URL = 'https://api.xing.com/v1/access_token' SCOPE_SEPARATOR = '+' From d1c415e9a852979c7916daf48352c13c710f859e Mon Sep 17 00:00:00 2001 From: Smamaxs Date: Thu, 8 May 2014 15:41:09 +0300 Subject: [PATCH 220/890] get email on login it is needed to make available merge user by email when it register by VK and username not email by authentification is by email --- social/backends/vk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/vk.py b/social/backends/vk.py index d27e29a88..56a3ae547 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -92,7 +92,7 @@ def get_user_details(self, response): last_name=response.get('last_name') ) return {'username': response.get('screen_name'), - 'email': '', + 'email': response.get('email', ''), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} From df175d910c061e3b4860ee83fb1ee413f069ba0b Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sat, 10 May 2014 16:18:12 -0700 Subject: [PATCH 221/890] Replace references to python-oauth2 with references to requests-oauthlib --- README.rst | 5 ++--- docs/installing.rst | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 16381d902..856678d10 100644 --- a/README.rst +++ b/README.rst @@ -142,8 +142,7 @@ Dependencies that **must** be met to use the application: - OpenId_ support depends on python-openid_ -- OAuth_ support depends on python-oauth2_ (despite the name, this is just for - OAuth1) +- OAuth_ support depends on requests-oauthlib_ - Several backends demand application registration on their corresponding sites and other dependencies like sqlalchemy_ on Flask and Webpy. @@ -274,7 +273,7 @@ check `django-social-auth LICENSE`_ for details: .. _Webpy: https://github.com/omab/python-social-auth/tree/master/social/apps/webpy_app .. _Tornado: http://www.tornadoweb.org/ .. _python-openid: http://pypi.python.org/pypi/python-openid/ -.. _python-oauth2: https://github.com/simplegeo/python-oauth2 +.. _requests-oauthlib: https://requests-oauthlib.readthedocs.org/ .. _sqlalchemy: http://www.sqlalchemy.org/ .. _pypi: http://pypi.python.org/pypi/python-social-auth/ .. _OpenStreetMap: http://www.openstreetmap.org diff --git a/docs/installing.rst b/docs/installing.rst index dd1fa03ac..09cb1d6bf 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -8,8 +8,7 @@ Dependencies that **must** be meet to use the application: - OpenId_ support depends on python-openid_ -- OAuth_ support depends on python-oauth2_ (despite the name, this is just for - OAuth1) +- OAuth_ support depends on requests-oauthlib_ - Several backends demands application registration on their corresponding sites and other dependencies like sqlalchemy_ on Flask and Webpy. @@ -45,5 +44,5 @@ Or:: .. _pypi: http://pypi.python.org/pypi/python-social-auth/ .. _github: https://github.com/omab/python-social-auth .. _python-openid: http://pypi.python.org/pypi/python-openid/ -.. _python-oauth2: https://github.com/simplegeo/python-oauth2 +.. _requests-oauthlib: https://requests-oauthlib.readthedocs.org/ .. _sqlalchemy: http://www.sqlalchemy.org/ From caef300ef1210894d6cb61612a2e290ff8f0da85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 12 May 2014 16:38:27 -0300 Subject: [PATCH 222/890] Remove unused import --- social/strategies/django_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index a7ac7e569..4798ba362 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponse from django.db.models import Model from django.contrib.contenttypes.models import ContentType from django.contrib.auth import authenticate From cd77c06fb49fd4f7b0e68ffee88b555b9da83033 Mon Sep 17 00:00:00 2001 From: swmerko Date: Tue, 13 May 2014 09:31:13 +0200 Subject: [PATCH 223/890] from http API to https API Non-SSL API deprecated: 27 June 2014, 10:00 (PDT) --- social/backends/flickr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/social/backends/flickr.py b/social/backends/flickr.py index cae8975c3..453ee69dd 100644 --- a/social/backends/flickr.py +++ b/social/backends/flickr.py @@ -8,9 +8,9 @@ class FlickrOAuth(BaseOAuth1): """Flickr OAuth authentication backend""" name = 'flickr' - AUTHORIZATION_URL = 'http://www.flickr.com/services/oauth/authorize' - REQUEST_TOKEN_URL = 'http://www.flickr.com/services/oauth/request_token' - ACCESS_TOKEN_URL = 'http://www.flickr.com/services/oauth/access_token' + AUTHORIZATION_URL = 'https://www.flickr.com/services/oauth/authorize' + REQUEST_TOKEN_URL = 'https://www.flickr.com/services/oauth/request_token' + ACCESS_TOKEN_URL = 'https://www.flickr.com/services/oauth/access_token' EXTRA_DATA = [ ('id', 'id'), ('username', 'username'), From 35ace101fbe1a1653b10647ad4b980fa4f752f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 13 May 2014 18:22:38 -0300 Subject: [PATCH 224/890] MongoEngine ORM support for flask applications --- docs/configuration/flask.rst | 19 +- examples/flask_me_example/__init__.py | 58 ++++++ examples/flask_me_example/manage.py | 20 ++ examples/flask_me_example/models/__init__.py | 2 + examples/flask_me_example/models/user.py | 16 ++ examples/flask_me_example/requirements.txt | 7 + examples/flask_me_example/routes/__init__.py | 2 + examples/flask_me_example/routes/main.py | 22 ++ examples/flask_me_example/settings.py | 58 ++++++ examples/flask_me_example/templates/base.html | 14 ++ examples/flask_me_example/templates/done.html | 24 +++ examples/flask_me_example/templates/home.html | 85 ++++++++ social/apps/flask_me_app/__init__.py | 5 + social/apps/flask_me_app/models.py | 45 +++++ social/apps/flask_me_app/routes.py | 39 ++++ social/apps/flask_me_app/template_filters.py | 25 +++ social/apps/flask_me_app/utils.py | 39 ++++ social/storage/mongoengine_orm.py | 188 ++++++++++++++++++ 18 files changed, 665 insertions(+), 3 deletions(-) create mode 100644 examples/flask_me_example/__init__.py create mode 100755 examples/flask_me_example/manage.py create mode 100644 examples/flask_me_example/models/__init__.py create mode 100644 examples/flask_me_example/models/user.py create mode 100644 examples/flask_me_example/requirements.txt create mode 100644 examples/flask_me_example/routes/__init__.py create mode 100644 examples/flask_me_example/routes/main.py create mode 100644 examples/flask_me_example/settings.py create mode 100644 examples/flask_me_example/templates/base.html create mode 100644 examples/flask_me_example/templates/done.html create mode 100644 examples/flask_me_example/templates/home.html create mode 100644 social/apps/flask_me_app/__init__.py create mode 100644 social/apps/flask_me_app/models.py create mode 100644 social/apps/flask_me_app/routes.py create mode 100644 social/apps/flask_me_app/template_filters.py create mode 100644 social/apps/flask_me_app/utils.py create mode 100644 social/storage/mongoengine_orm.py diff --git a/docs/configuration/flask.rst b/docs/configuration/flask.rst index 044859882..7959dd777 100644 --- a/docs/configuration/flask.rst +++ b/docs/configuration/flask.rst @@ -8,20 +8,26 @@ details on how to enable this application on Flask. Dependencies ------------ -The `Flask built-in app` depends on sqlalchemy_, there's no support for others -ORMs yet but pull-requests are welcome. +The `Flask built-in app` depends on sqlalchemy_, there's initial support for +MongoEngine_ ORM too (check below for more details). Enabling the application ------------------------ -The application defines a `Flask Blueprint`_, which needs to be registered once +The applications define a `Flask Blueprint`_, which needs to be registered once the Flask app is configured:: from social.apps.flask_app.routes import social_auth app.register_blueprint(social_auth) +For MongoEngine_ version:: + + from social.apps.flask_me_app.routes import social_auth + + app.register_blueprint(social_auth) + Models Setup ------------ @@ -35,6 +41,12 @@ and the database are defined, call ``init_social`` to register the models:: init_social(app, db) +For MongoEngine_:: + + from social.apps.flask_me_app.models import init_social + + init_social(app, db) + So far I wasn't able to find another way to define the models on another way rather than making it as a side-effect of calling this function since the database is not available and ``current_app`` cannot be used on init time, just @@ -121,3 +133,4 @@ handlers won't be called. .. _sqlalchemy: http://www.sqlalchemy.org/ .. _exceptions: https://github.com/omab/python-social-auth/blob/master/social/exceptions.py .. _errorhandler: http://flask.pocoo.org/docs/api/#flask.Flask.errorhandler +.. _MongoEngine: http://mongoengine.org diff --git a/examples/flask_me_example/__init__.py b/examples/flask_me_example/__init__.py new file mode 100644 index 000000000..f99aa29a1 --- /dev/null +++ b/examples/flask_me_example/__init__.py @@ -0,0 +1,58 @@ +import sys + +from flask import Flask, g +from flask.ext import login +from flask.ext.mongoengine import MongoEngine + +sys.path.append('../..') + +from social.apps.flask_me_app.routes import social_auth +from social.apps.flask_me_app.models import init_social +from social.apps.flask_me_app.template_filters import backends + + +# App +app = Flask(__name__) +app.config.from_object('flask_me_example.settings') + +try: + app.config.from_object('flask_me_example.local_settings') +except ImportError: + pass + +# DB +db = MongoEngine(app) +app.register_blueprint(social_auth) +init_social(app, db) + +login_manager = login.LoginManager() +login_manager.login_view = 'main' +login_manager.login_message = '' +login_manager.init_app(app) + +from flask_me_example import models +from flask_me_example import routes + + +@login_manager.user_loader +def load_user(userid): + try: + return models.user.User.objects.get(id=userid) + except (TypeError, ValueError): + pass + + +@app.before_request +def global_user(): + g.user = login.current_user + + +@app.context_processor +def inject_user(): + try: + return {'user': g.user} + except AttributeError: + return {'user': None} + + +app.context_processor(backends) diff --git a/examples/flask_me_example/manage.py b/examples/flask_me_example/manage.py new file mode 100755 index 000000000..b8948ef15 --- /dev/null +++ b/examples/flask_me_example/manage.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import sys + +from flask.ext.script import Server, Manager, Shell + +sys.path.append('..') + +from flask_me_example import app, db + + +manager = Manager(app) +manager.add_command('runserver', Server()) +manager.add_command('shell', Shell(make_context=lambda: { + 'app': app, + 'db': db +})) + + +if __name__ == '__main__': + manager.run() diff --git a/examples/flask_me_example/models/__init__.py b/examples/flask_me_example/models/__init__.py new file mode 100644 index 000000000..cec669418 --- /dev/null +++ b/examples/flask_me_example/models/__init__.py @@ -0,0 +1,2 @@ +from flask_me_example.models import user +from social.apps.flask_me_app import models diff --git a/examples/flask_me_example/models/user.py b/examples/flask_me_example/models/user.py new file mode 100644 index 000000000..035b5276b --- /dev/null +++ b/examples/flask_me_example/models/user.py @@ -0,0 +1,16 @@ +from mongoengine import StringField, EmailField, BooleanField + +from flask.ext.login import UserMixin + +from flask_me_example import db + + +class User(db.Document, UserMixin): + username = StringField(max_length=200) + password = StringField(max_length=200, default='') + name = StringField(max_length=100) + email = EmailField() + active = BooleanField(default=True) + + def is_active(self): + return self.active diff --git a/examples/flask_me_example/requirements.txt b/examples/flask_me_example/requirements.txt new file mode 100644 index 000000000..e79691a4a --- /dev/null +++ b/examples/flask_me_example/requirements.txt @@ -0,0 +1,7 @@ +Flask +Flask-Login +Flask-Script +Werkzeug +Jinja2 +mongoengine==0.8.4 +python-social-auth diff --git a/examples/flask_me_example/routes/__init__.py b/examples/flask_me_example/routes/__init__.py new file mode 100644 index 000000000..915d4931c --- /dev/null +++ b/examples/flask_me_example/routes/__init__.py @@ -0,0 +1,2 @@ +from flask_me_example.routes import main +from social.apps.flask_me_app import routes diff --git a/examples/flask_me_example/routes/main.py b/examples/flask_me_example/routes/main.py new file mode 100644 index 000000000..2a2ed4e26 --- /dev/null +++ b/examples/flask_me_example/routes/main.py @@ -0,0 +1,22 @@ +from flask import render_template, redirect +from flask.ext.login import login_required, logout_user + +from flask_me_example import app + + +@app.route('/') +def main(): + return render_template('home.html') + + +@login_required +@app.route('/done/') +def done(): + return render_template('done.html') + + +@app.route('/logout') +def logout(): + """Logout view""" + logout_user() + return redirect('/') diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py new file mode 100644 index 000000000..6bd214770 --- /dev/null +++ b/examples/flask_me_example/settings.py @@ -0,0 +1,58 @@ +from flask_me_example import app + + +app.debug = True + +SECRET_KEY = 'random-secret-key' +SESSION_COOKIE_NAME = 'psa_session' +DEBUG = False + +MONGODB_SETTINGS = {'DB': 'psa_db'} + +DEBUG_TB_INTERCEPT_REDIRECTS = False +SESSION_PROTECTION = 'strong' + +SOCIAL_AUTH_LOGIN_URL = '/' +SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/' +SOCIAL_AUTH_USER_MODEL = 'flask_me_example.models.user.User' +SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + 'social.backends.open_id.OpenIdAuth', + 'social.backends.google.GoogleOpenId', + 'social.backends.google.GoogleOAuth2', + 'social.backends.google.GoogleOAuth', + 'social.backends.twitter.TwitterOAuth', + 'social.backends.yahoo.YahooOpenId', + 'social.backends.stripe.StripeOAuth2', + 'social.backends.persona.PersonaAuth', + 'social.backends.facebook.FacebookOAuth2', + 'social.backends.facebook.FacebookAppOAuth2', + 'social.backends.yahoo.YahooOAuth', + 'social.backends.angel.AngelOAuth2', + 'social.backends.behance.BehanceOAuth2', + 'social.backends.bitbucket.BitbucketOAuth', + 'social.backends.box.BoxOAuth2', + 'social.backends.linkedin.LinkedinOAuth', + 'social.backends.github.GithubOAuth2', + 'social.backends.foursquare.FoursquareOAuth2', + 'social.backends.instagram.InstagramOAuth2', + 'social.backends.live.LiveOAuth2', + 'social.backends.vk.VKOAuth2', + 'social.backends.dailymotion.DailymotionOAuth2', + 'social.backends.disqus.DisqusOAuth2', + 'social.backends.dropbox.DropboxOAuth', + 'social.backends.evernote.EvernoteSandboxOAuth', + 'social.backends.fitbit.FitbitOAuth', + 'social.backends.flickr.FlickrOAuth', + 'social.backends.livejournal.LiveJournalOpenId', + 'social.backends.soundcloud.SoundcloudOAuth2', + 'social.backends.lastfm.LastFmAuth', + 'social.backends.thisismyjam.ThisIsMyJamOAuth1', + 'social.backends.stocktwits.StocktwitsOAuth2', + 'social.backends.tripit.TripItOAuth', + 'social.backends.clef.ClefOAuth2', + 'social.backends.twilio.TwilioAuth', + 'social.backends.xing.XingOAuth', + 'social.backends.yandex.YandexOAuth2', + 'social.backends.podio.PodioOAuth2', + 'social.backends.reddit.RedditOAuth2', +) diff --git a/examples/flask_me_example/templates/base.html b/examples/flask_me_example/templates/base.html new file mode 100644 index 000000000..86db50440 --- /dev/null +++ b/examples/flask_me_example/templates/base.html @@ -0,0 +1,14 @@ + + + + Social + + + + {% block content %}{% endblock %} + {% block scripts %}{% endblock %} + + + + + diff --git a/examples/flask_me_example/templates/done.html b/examples/flask_me_example/templates/done.html new file mode 100644 index 000000000..ccabf53ec --- /dev/null +++ b/examples/flask_me_example/templates/done.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

          You are logged in as {{ user.username }}!

          + +

          Associated:

          +{% for assoc in backends.associated %} +
          + {{ assoc.provider }} +
          + +
          +
          +{% endfor %} + +

          Associate:

          +
            + {% for name in backends.not_associated %} +
          • + {{ name }} +
          • + {% endfor %} +
          +{% endblock %} diff --git a/examples/flask_me_example/templates/home.html b/examples/flask_me_example/templates/home.html new file mode 100644 index 000000000..c5c2a3ed7 --- /dev/null +++ b/examples/flask_me_example/templates/home.html @@ -0,0 +1,85 @@ +{% extends "base.html" %} + +{% block content %} +Google OAuth2
          +Google OAuth
          +Google OpenId
          +Twitter OAuth
          +Yahoo OpenId
          +Yahoo OAuth
          +Stripe OAuth2
          +Facebook OAuth2
          +Facebook App
          +Angel OAuth2
          +Behance OAuth2
          +Bitbucket OAuth
          +Box OAuth2
          +LinkedIn OAuth
          +Github OAuth2
          +Foursquare OAuth2
          +Instagram OAuth2
          +Live OAuth2
          +VK.com OAuth2
          +Dailymotion OAuth2
          +Disqus OAuth2
          +Dropbox OAuth
          +Evernote OAuth (sandbox mode)
          +Fitbit OAuth
          +Flickr OAuth
          +Soundcloud OAuth2
          +LastFm
          +ThisIsMyJam OAuth1
          +Stocktwits OAuth2
          +Tripit OAuth
          +Clef OAuth2
          +Twilio
          +Xing OAuth
          +Yandex OAuth2
          +Podio OAuth2
          + +
          +
          + + + +
          +
          + +
          +
          + + + +
          +
          + +
          + + Persona +
          +{% endblock %} + +{% block scripts %} + + + +{% endblock %} diff --git a/social/apps/flask_me_app/__init__.py b/social/apps/flask_me_app/__init__.py new file mode 100644 index 000000000..125e69e7c --- /dev/null +++ b/social/apps/flask_me_app/__init__.py @@ -0,0 +1,5 @@ +from social.strategies.utils import set_current_strategy_getter +from social.apps.flask_me_app.utils import load_strategy + + +set_current_strategy_getter(load_strategy) diff --git a/social/apps/flask_me_app/models.py b/social/apps/flask_me_app/models.py new file mode 100644 index 000000000..fe91d62e2 --- /dev/null +++ b/social/apps/flask_me_app/models.py @@ -0,0 +1,45 @@ +"""Flask SQLAlchemy ORM models for Social Auth""" +from mongoengine import ReferenceField + +from social.utils import setting_name, module_member +from social.storage.mongoengine_orm import MongoengineUserMixin, \ + MongoengineAssociationMixin, \ + MongoengineNonceMixin, \ + MongoengineCodeMixin, \ + BaseMongoengineStorage + + +class FlaskStorage(BaseMongoengineStorage): + user = None + nonce = None + association = None + code = None + + +def init_social(app, db): + User = module_member(app.config[setting_name('USER_MODEL')]) + + class UserSocialAuth(db.Document, MongoengineUserMixin): + """Social Auth association model""" + user = ReferenceField(User) + + @classmethod + def user_model(cls): + return User + + class Nonce(db.Document, MongoengineNonceMixin): + """One use numbers""" + pass + + class Association(db.Document, MongoengineAssociationMixin): + """OpenId account association""" + pass + + class Code(db.Document, MongoengineCodeMixin): + pass + + # Set the references in the storage class + FlaskStorage.user = UserSocialAuth + FlaskStorage.nonce = Nonce + FlaskStorage.association = Association + FlaskStorage.code = Code diff --git a/social/apps/flask_me_app/routes.py b/social/apps/flask_me_app/routes.py new file mode 100644 index 000000000..65ad645e6 --- /dev/null +++ b/social/apps/flask_me_app/routes.py @@ -0,0 +1,39 @@ +from flask import g, Blueprint, request +from flask.ext.login import login_required, login_user + +from social.actions import do_auth, do_complete, do_disconnect +from social.apps.flask_me_app.utils import strategy + + +social_auth = Blueprint('social', __name__) + + +@social_auth.route('/login//', methods=('GET', 'POST')) +@strategy('social.complete') +def auth(backend): + return do_auth(g.strategy) + + +@social_auth.route('/complete//', methods=('GET', 'POST')) +@strategy('social.complete') +def complete(backend, *args, **kwargs): + """Authentication complete view, override this view if transaction + management doesn't suit your needs.""" + return do_complete(g.strategy, login=do_login, user=g.user, + *args, **kwargs) + + +@social_auth.route('/disconnect//', methods=('POST',)) +@social_auth.route('/disconnect///', + methods=('POST',)) +@login_required +@strategy() +def disconnect(backend, association_id=None): + """Disconnects given backend from current logged in user.""" + return do_disconnect(g.strategy, g.user, association_id) + + +def do_login(strategy, user, social_user): + return login_user(user, remember=request.cookies.get('remember') or + request.args.get('remember') or + request.form.get('remember') or False) diff --git a/social/apps/flask_me_app/template_filters.py b/social/apps/flask_me_app/template_filters.py new file mode 100644 index 000000000..ec561cdbf --- /dev/null +++ b/social/apps/flask_me_app/template_filters.py @@ -0,0 +1,25 @@ +from flask import g, request + +from social.backends.utils import user_backends_data +from social.apps.flask_me_app.utils import get_helper + + +def backends(): + """Load Social Auth current user data to context under the key 'backends'. + Will return the output of social.backends.utils.user_backends_data.""" + return { + 'backends': user_backends_data(g.user, + get_helper('AUTHENTICATION_BACKENDS'), + get_helper('STORAGE', do_import=True)) + } + + +def login_redirect(): + """Load current redirect to context.""" + value = request.form.get('next', '') or \ + request.args.get('next', '') + return { + 'REDIRECT_FIELD_NAME': 'next', + 'REDIRECT_FIELD_VALUE': value, + 'REDIRECT_QUERYSTRING': value and ('next=' + value) or '' + } diff --git a/social/apps/flask_me_app/utils.py b/social/apps/flask_me_app/utils.py new file mode 100644 index 000000000..c0fd8cbf0 --- /dev/null +++ b/social/apps/flask_me_app/utils.py @@ -0,0 +1,39 @@ +from functools import wraps + +from flask import current_app, url_for, g, request + +from social.utils import module_member, setting_name +from social.strategies.utils import get_strategy + + +DEFAULTS = { + 'STORAGE': 'social.apps.flask_me_app.models.FlaskStorage', + 'STRATEGY': 'social.strategies.flask_strategy.FlaskStrategy' +} + + +def get_helper(name, do_import=False): + config = current_app.config.get(setting_name(name), + DEFAULTS.get(name, None)) + return do_import and module_member(config) or config + + +def load_strategy(*args, **kwargs): + backends = get_helper('AUTHENTICATION_BACKENDS') + strategy = get_helper('STRATEGY') + storage = get_helper('STORAGE') + return get_strategy(backends, strategy, storage, *args, **kwargs) + + +def strategy(redirect_uri=None): + def decorator(func): + @wraps(func) + def wrapper(backend, *args, **kwargs): + uri = redirect_uri + if uri and not uri.startswith('/'): + uri = url_for(uri, backend=backend) + g.strategy = load_strategy(request=request, backend=backend, + redirect_uri=uri, *args, **kwargs) + return func(backend, *args, **kwargs) + return wrapper + return decorator diff --git a/social/storage/mongoengine_orm.py b/social/storage/mongoengine_orm.py new file mode 100644 index 000000000..e74f1f1c2 --- /dev/null +++ b/social/storage/mongoengine_orm.py @@ -0,0 +1,188 @@ +import base64 +import six + +from mongoengine import DictField, IntField, StringField, \ + EmailField, BooleanField +from mongoengine.queryset import OperationError + +from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ + CodeMixin, BaseStorage + + +UNUSABLE_PASSWORD = '!' # Borrowed from django 1.4 + + +class MongoengineUserMixin(UserMixin): + """Social Auth association model""" + user = None + provider = StringField(max_length=32) + uid = StringField(max_length=255, unique_with='provider') + extra_data = DictField() + + def str_id(self): + return str(self.id) + + @classmethod + def get_social_auth_for_user(cls, user, provider=None, id=None): + qs = cls.objects + if provider: + qs = qs.filter(provider=provider) + if id: + qs = qs.filter(id=id) + return qs.filter(user=user.id) + + @classmethod + def create_social_auth(cls, user, uid, provider): + if not isinstance(type(uid), six.string_types): + uid = str(uid) + return cls.objects.create(user=user.id, uid=uid, provider=provider) + + @classmethod + def username_max_length(cls): + username_field = cls.username_field() + field = getattr(cls.user_model(), username_field) + return field.max_length + + @classmethod + def username_field(cls): + return getattr(cls.user_model(), 'USERNAME_FIELD', 'username') + + @classmethod + def create_user(cls, *args, **kwargs): + kwargs['password'] = UNUSABLE_PASSWORD + if 'email' in kwargs: + # Empty string makes email regex validation fail + kwargs['email'] = kwargs['email'] or None + return cls.user_model().objects.create(*args, **kwargs) + + @classmethod + def allowed_to_disconnect(cls, user, backend_name, association_id=None): + if association_id is not None: + qs = cls.objects.filter(id__ne=association_id) + else: + qs = cls.objects.filter(provider__ne=backend_name) + qs = qs.filter(user=user) + + if hasattr(user, 'has_usable_password'): + valid_password = user.has_usable_password() + else: + valid_password = True + + return valid_password or qs.count() > 0 + + @classmethod + def changed(cls, user): + user.save() + + def set_extra_data(self, extra_data=None): + if super(MongoengineUserMixin, self).set_extra_data(extra_data): + self.save() + + @classmethod + def disconnect(cls, entry): + entry.delete() + + @classmethod + def user_exists(cls, *args, **kwargs): + """ + Return True/False if a User instance exists with the given arguments. + Arguments are directly passed to filter() manager method. + """ + if 'username' in kwargs: + kwargs[cls.username_field()] = kwargs.pop('username') + return cls.user_model().objects.filter(*args, **kwargs).count() > 0 + + @classmethod + def get_username(cls, user): + return getattr(user, cls.username_field(), None) + + @classmethod + def get_user(cls, pk): + try: + return cls.user_model().objects.get(id=pk) + except cls.user_model().DoesNotExist: + return None + + @classmethod + def get_users_by_email(cls, email): + return cls.user_model().objects.filter(email__iexact=email) + + @classmethod + def get_social_auth(cls, provider, uid): + if not isinstance(uid, six.string_types): + uid = str(uid) + try: + return cls.objects.get(provider=provider, uid=uid) + except cls.DoesNotExist: + return None + + +class MongoengineNonceMixin(NonceMixin): + """One use numbers""" + server_url = StringField(max_length=255) + timestamp = IntField() + salt = StringField(max_length=40) + + @classmethod + def use(cls, server_url, timestamp, salt): + return cls.objects.get_or_create(server_url=server_url, + timestamp=timestamp, + salt=salt)[1] + + +class MongoengineAssociationMixin(AssociationMixin): + """OpenId account association""" + server_url = StringField(max_length=255) + handle = StringField(max_length=255) + secret = StringField(max_length=255) # Stored base64 encoded + issued = IntField() + lifetime = IntField() + assoc_type = StringField(max_length=64) + + @classmethod + def store(cls, server_url, association): + # Don't use get_or_create because issued cannot be null + try: + assoc = cls.objects.get(server_url=server_url, + handle=association.handle) + except cls.DoesNotExist: + assoc = cls(server_url=server_url, + handle=association.handle) + assoc.secret = base64.encodestring(association.secret) + assoc.issued = association.issued + assoc.lifetime = association.lifetime + assoc.assoc_type = association.assoc_type + assoc.save() + + @classmethod + def get(cls, *args, **kwargs): + return cls.objects.filter(*args, **kwargs) + + @classmethod + def remove(cls, ids_to_delete): + cls.objects.filter(pk__in=ids_to_delete).delete() + + +class MongoengineCodeMixin(CodeMixin): + email = EmailField() + code = StringField(max_length=32) + verified = BooleanField(default=False) + + @classmethod + def get_code(cls, code): + try: + return cls.objects.get(code=code) + except cls.DoesNotExist: + return None + + +class BaseMongoengineStorage(BaseStorage): + user = MongoengineUserMixin + nonce = MongoengineNonceMixin + association = MongoengineAssociationMixin + code = MongoengineCodeMixin + + @classmethod + def is_integrity_error(cls, exception): + return exception.__class__ is OperationError and \ + 'E11000' in exception.message From e14341cedf31c6764a4c62965b6dd76656aebeb4 Mon Sep 17 00:00:00 2001 From: Jason Sanford Date: Wed, 14 May 2014 23:59:39 -0400 Subject: [PATCH 225/890] Get started with MapMyFitness OAuth2 --- social/backends/mapmyfitness.py | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 social/backends/mapmyfitness.py diff --git a/social/backends/mapmyfitness.py b/social/backends/mapmyfitness.py new file mode 100644 index 000000000..d4e68b21a --- /dev/null +++ b/social/backends/mapmyfitness.py @@ -0,0 +1,48 @@ +""" +MapMyFitness OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/mapmyfitness.html +""" +from social.backends.oauth import BaseOAuth2 + + +class MapMyFitnessOAuth2(BaseOAuth2): + """MapMyFitness OAuth authentication backend""" + name = 'mapmyfitness' + AUTHORIZATION_URL = 'https://www.mapmyfitness.com/v7.0/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://oauth2-api.mapmyapi.com/v7.0/oauth2/access_token' + REQUEST_TOKEN_METHOD = 'POST' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('refresh_token', 'refresh_token'), + ] + + def auth_headers(self): + key = self.get_key_and_secret()[0] + return { + 'Api-Key': key + } + + def get_user_id(self, details, response): + return response['id'] + + def get_user_details(self, response): + first = response.get('first_name', '') + last = response.get('last_name', '') + full = (first + last).strip() + return { + 'username': response['username'], + 'email': response['email'], + 'fullname': full, + 'first_name': first, + 'last_name': last, + } + + def user_data(self, access_token, *args, **kwargs): + key = self.get_key_and_secret()[0] + url = 'https://oauth2-api.mapmyapi.com/v7.0/user/self/' + headers = { + 'Authorization': 'Bearer {}'.format(access_token), + 'Api-Key': key + } + return self.get_json(url, headers=headers) From f982555ac6999dd315a1a5c51d0bc092af2811d4 Mon Sep 17 00:00:00 2001 From: Jason Sanford Date: Thu, 15 May 2014 14:28:57 -0400 Subject: [PATCH 226/890] Test MapMyFitness backend --- social/tests/backends/test_mapmyfitness.py | 153 +++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 social/tests/backends/test_mapmyfitness.py diff --git a/social/tests/backends/test_mapmyfitness.py b/social/tests/backends/test_mapmyfitness.py new file mode 100644 index 000000000..b57bdcafe --- /dev/null +++ b/social/tests/backends/test_mapmyfitness.py @@ -0,0 +1,153 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class MapMyFitnessOAuth2Test(OAuth2Test): + backend_path = 'social.backends.mapmyfitness.MapMyFitnessOAuth2' + user_data_url = 'https://oauth2-api.mapmyapi.com/v7.0/user/self/' + expected_username = 'FredFlinstone' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'Bearer', + 'expires_in': 4000000, + 'refresh_token': 'bambaz', + 'scope': 'read' + }) + user_data_body = json.dumps({ + 'last_name': 'Flinstone', + 'weight': 91.17206637, + 'communication': { + 'promotions': True, + 'newsletter': True, + 'system_messages': True + }, + 'height': 1.778, + 'token_type': 'Bearer', + 'id': 112233, + 'date_joined': '2011-08-26T06:06:19+00:00', + 'first_name': 'Fred', + 'display_name': 'Fred Flinstone', + 'display_measurement_system': 'imperial', + 'expires_in': 4000000, + '_links': { + 'stats': [ + { + 'href': '/v7.0/user_stats/112233/?aggregate_by_period=month', + 'id': '112233', + 'name': 'month' + }, + { + 'href': '/v7.0/user_stats/112233/?aggregate_by_period=year', + 'id': '112233', + 'name': 'year' + }, + { + 'href': '/v7.0/user_stats/112233/?aggregate_by_period=day', + 'id': '112233', + 'name': 'day' + }, + { + 'href': '/v7.0/user_stats/112233/?aggregate_by_period=week', + 'id': '112233', + 'name': 'week' + }, + { + 'href': '/v7.0/user_stats/112233/?aggregate_by_period=lifetime', + 'id': '112233', + 'name': 'lifetime' + } + ], + 'friendships': [ + { + 'href': '/v7.0/friendship/?from_user=112233' + } + ], + 'privacy': [ + { + 'href': '/v7.0/privacy_option/3/', + 'id': '3', + 'name': 'profile' + }, + { + 'href': '/v7.0/privacy_option/3/', + 'id': '3', + 'name': 'workout' + }, + { + 'href': '/v7.0/privacy_option/3/', + 'id': '3', + 'name': 'activity_feed' + }, + { + 'href': '/v7.0/privacy_option/1/', + 'id': '1', + 'name': 'food_log' + }, + { + 'href': '/v7.0/privacy_option/3/', + 'id': '3', + 'name': 'email_search' + }, + { + 'href': '/v7.0/privacy_option/3/', + 'id': '3', + 'name': 'route' + } + ], + 'image': [ + { + 'href': '/v7.0/user_profile_photo/112233/', + 'id': '112233', + 'name': 'user_profile_photo' + } + ], + 'documentation': [ + { + 'href': 'https://www.mapmyapi.com/docs/User' + } + ], + 'workouts': [ + { + 'href': '/v7.0/workout/?user=112233&order_by=-start_datetime' + } + ], + 'deactivation': [ + { + 'href': '/v7.0/user_deactivation/' + } + ], + 'self': [ + { + 'href': '/v7.0/user/112233/', + 'id': '112233' + } + ] + }, + 'location': { + 'country': 'US', + 'region': 'NC', + 'locality': 'Bedrock', + 'address': '150 Dinosaur Ln' + }, + 'last_login': '2014-02-23T22:36:52+00:00', + 'email': 'fredflinstone@gmail.com', + 'username': 'FredFlinstone', + 'sharing': { + 'twitter': False, + 'facebook': False + }, + 'scope': 'read', + 'refresh_token': 'bambaz', + 'last_initial': 'S.', + 'access_token': 'foobar', + 'gender': 'M', + 'time_zone': 'America/Denver', + 'birthdate': '1983-04-15' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 53c85d44306aec5c7b991107dc2d3b456f10b9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 15 May 2014 18:50:14 -0300 Subject: [PATCH 227/890] Change priority for new user redirect location Gives more importance to the value of SOCIAL_AUTH_NEW_USER_REDIRECT_URL ignoring "?next=..." value if it's present. Fixes #276 --- social/actions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/actions.py b/social/actions.py index 376e7e5e2..53a848693 100644 --- a/social/actions.py +++ b/social/actions.py @@ -64,8 +64,9 @@ def do_complete(strategy, login, user=None, redirect_name='next', social_user.provider) if is_new: - url = setting_url(strategy, redirect_value, + url = setting_url(strategy, 'NEW_USER_REDIRECT_URL', + redirect_value, 'LOGIN_REDIRECT_URL') else: url = setting_url(strategy, redirect_value, From 9bf263d1b1eb03bc29c6583a87de99a435f9d051 Mon Sep 17 00:00:00 2001 From: Jason Sanford Date: Thu, 15 May 2014 21:29:53 -0400 Subject: [PATCH 228/890] Document MapMyFitness --- docs/backends/mapmyfitness.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/backends/mapmyfitness.rst diff --git a/docs/backends/mapmyfitness.rst b/docs/backends/mapmyfitness.rst new file mode 100644 index 000000000..1482d9667 --- /dev/null +++ b/docs/backends/mapmyfitness.rst @@ -0,0 +1,13 @@ +MapMyFitness +========= + +MapMyFitness uses OAuth v2 for authentication. + +- Register a new application at the `MapMyFitness API`_, and + +- fill ``key`` and ``secret`` values in the settings:: + + SOCIAL_AUTH_MAPMYFITNESS_KEY = '' + SOCIAL_AUTH_MAPMYFITNESS_SECRET = '' + +.. _MapMyFitness API: https://www.mapmyapi.com From 5551086c3b3708a1768104cf4feb013d5fde3bf5 Mon Sep 17 00:00:00 2001 From: Jason Sanford Date: Thu, 15 May 2014 21:35:53 -0400 Subject: [PATCH 229/890] Python 2.6-friendly string formatting. --- social/backends/mapmyfitness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/mapmyfitness.py b/social/backends/mapmyfitness.py index d4e68b21a..c1b714ce7 100644 --- a/social/backends/mapmyfitness.py +++ b/social/backends/mapmyfitness.py @@ -42,7 +42,7 @@ def user_data(self, access_token, *args, **kwargs): key = self.get_key_and_secret()[0] url = 'https://oauth2-api.mapmyapi.com/v7.0/user/self/' headers = { - 'Authorization': 'Bearer {}'.format(access_token), + 'Authorization': 'Bearer {0}'.format(access_token), 'Api-Key': key } return self.get_json(url, headers=headers) From bf7b8de2cbd31e5e3b95f8ca93d8804880a84085 Mon Sep 17 00:00:00 2001 From: Ryan Choi Date: Thu, 15 May 2014 18:58:01 -0700 Subject: [PATCH 230/890] spotify oauth --- docs/backends/index.rst | 1 + docs/backends/spotify.rst | 25 ++++++++++ docs/intro.rst | 2 + examples/django_example/example/settings.py | 1 + .../example/templates/home.html | 1 + social/backends/spotify.py | 50 +++++++++++++++++++ 6 files changed, 80 insertions(+) create mode 100644 docs/backends/spotify.rst create mode 100644 social/backends/spotify.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index fc011f004..7a5e5f460 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -93,6 +93,7 @@ Social backends shopify skyrock soundcloud + spotify suse stackoverflow steam diff --git a/docs/backends/spotify.rst b/docs/backends/spotify.rst new file mode 100644 index 000000000..3f1c0dd01 --- /dev/null +++ b/docs/backends/spotify.rst @@ -0,0 +1,25 @@ +Spotify +======= + +Spotify supports OAuth 2. + +- Register a new application at `Spotify Web API`_, and follow the + instructions below. + +OAuth2 +------ + +Add the Spotify OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.spotify.SpotifyOAuth2', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_SPOTIFY_OAUTH2_KEY = '' + SOCIAL_AUTH_SPOTIFY_OAUTH2_SECRET = '' + +.. _Spotify Web API: https://developer.spotify.com/spotify-web-api diff --git a/docs/intro.rst b/docs/intro.rst index d43bb7cb4..03335f31c 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -67,6 +67,7 @@ or extend current one): * Shopify_ OAuth2 * Skyrock_ OAuth1 * Soundcloud_ OAuth2 + * Spotify_ OAuth2 * ThisIsMyJam_ OAuth1 * Stackoverflow_ OAuth2 * Steam_ OpenId @@ -140,6 +141,7 @@ section. .. _Shopify: http://shopify.com .. _Skyrock: https://skyrock.com .. _Soundcloud: https://soundcloud.com +.. _Spotify: https://www.spotify.com .. _ThisIsMyJam: https://thisismyjam.com .. _Stocktwits: https://stocktwits.com .. _Stripe: https://stripe.com diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 3eab0b4f3..f76dc7756 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -168,6 +168,7 @@ 'social.backends.runkeeper.RunKeeperOAuth2', 'social.backends.skyrock.SkyrockOAuth', 'social.backends.soundcloud.SoundcloudOAuth2', + 'social.backends.spotify.SpotifyOAuth2', 'social.backends.stackoverflow.StackoverflowOAuth2', 'social.backends.steam.SteamOpenId', 'social.backends.stocktwits.StocktwitsOAuth2', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index abb429430..0f9f4b6cf 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -48,6 +48,7 @@ Runkeeper OAuth2
          Skyrock OAuth1
          Soundcloud OAuth2
          +Spotify OAuth2
          Stackoverflow OAuth2
          Steam OpenId
          Stocktwits OAuth2
          diff --git a/social/backends/spotify.py b/social/backends/spotify.py new file mode 100644 index 000000000..dafbe7528 --- /dev/null +++ b/social/backends/spotify.py @@ -0,0 +1,50 @@ +""" +Spotify backend, docs at: + https://developer.spotify.com/spotify-web-api/ + https://developer.spotify.com/spotify-web-api/authorization-guide/ +""" +from re import sub + +import base64 + +from social.p3 import urlencode +from social.backends.oauth import BaseOAuth2 + +class SpotifyOAuth2(BaseOAuth2): + name = 'spotify' + SCOPE_SEPARATOR = ' ' + ID_KEY = 'id' + AUTHORIZATION_URL = 'https://accounts.spotify.com/authorize' + ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token' + ACCESS_TOKEN_METHOD = 'POST' +# RESPONSE_TYPE = 'token' + REDIRECT_STATE = False + STATE_PARAMETER = False +# EXTRA_DATA = [ +# ('id', 'username'), +# ] + + def auth_headers(self): + return { + 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( + ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) + )) + } + + def get_user_details(self, response): + """Return user details from Spotify account""" + fullname, first_name, last_name = self.get_user_names( + response.get('display_name') + ) + return {'username': response.get('id'), + 'email': response.get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://api.spotify.com/v1/me', + headers={'Authorization': 'Bearer {0}'.format(access_token)} + ) \ No newline at end of file From 8ce03af2ec0af51e317bb65ee4ce3ce5b742a36c Mon Sep 17 00:00:00 2001 From: Ryan Choi Date: Thu, 15 May 2014 19:57:14 -0700 Subject: [PATCH 231/890] remove commented code for spotify --- social/backends/spotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/spotify.py b/social/backends/spotify.py index dafbe7528..9bf97ebc1 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -17,7 +17,6 @@ class SpotifyOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://accounts.spotify.com/authorize' ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token' ACCESS_TOKEN_METHOD = 'POST' -# RESPONSE_TYPE = 'token' REDIRECT_STATE = False STATE_PARAMETER = False # EXTRA_DATA = [ @@ -44,6 +43,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" + return self.get_json( 'https://api.spotify.com/v1/me', headers={'Authorization': 'Bearer {0}'.format(access_token)} From 552a3501c1c62c3d3d212dc647f9b75289ca876b Mon Sep 17 00:00:00 2001 From: Jason Sanford Date: Thu, 15 May 2014 23:30:06 -0400 Subject: [PATCH 232/890] Add links. --- README.rst | 2 ++ docs/backends/index.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 16381d902..235c1487f 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,7 @@ or current ones extended): * Livejournal_ OpenId * LoginRadius_ OAuth2 and Application Auth * Mailru_ OAuth2 + * MapMyFitness_ OAuth2 * Mendeley_ OAuth1 http://mendeley.com * Mixcloud_ OAuth2 * `Mozilla Persona`_ @@ -231,6 +232,7 @@ check `django-social-auth LICENSE`_ for details: .. _Live: https://live.com .. _Livejournal: http://livejournal.com .. _Mailru: https://mail.ru +.. _MapMyFitness: http://www.mapmyfitness.com/ .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _Odnoklassniki: http://www.odnoklassniki.ru diff --git a/docs/backends/index.rst b/docs/backends/index.rst index fc011f004..4733b74fa 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -78,6 +78,7 @@ Social backends live loginradius mailru + mapmyfitness mendeley mixcloud odnoklassnikiru From f120f079c752b66207a708af5dcb70085a03182d Mon Sep 17 00:00:00 2001 From: Ryan Choi Date: Sat, 17 May 2014 00:58:29 -0700 Subject: [PATCH 233/890] oauth for beats --- .settings/org.eclipse.core.resources.prefs | 2 + docs/backends/beats.rst | 25 +++++++++ docs/backends/index.rst | 1 + docs/intro.rst | 2 + examples/django_example/example/settings.py | 1 + .../example/templates/home.html | 1 + social/backends/beats.py | 54 +++++++++++++++++++ social/backends/oauth.py | 5 ++ 8 files changed, 91 insertions(+) create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 docs/backends/beats.rst create mode 100644 social/backends/beats.py diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..8de73cc16 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//social/backends/spotify.py=utf-8 diff --git a/docs/backends/beats.rst b/docs/backends/beats.rst new file mode 100644 index 000000000..15cadfdbc --- /dev/null +++ b/docs/backends/beats.rst @@ -0,0 +1,25 @@ +Beats +======= + +Beats supports OAuth 2. + +- Register a new application at `Beats Music API`_, and follow the + instructions below. + +OAuth2 +------ + +Add the Beats OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.beats.BeatsOAuth2', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_BEATS_OAUTH2_KEY = '' + SOCIAL_AUTH_BEATS_OAUTH2_SECRET = '' + +.. _Beats Music API: https://developer.beatsmusic.com/docs diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 7a5e5f460..88b6437fd 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -50,6 +50,7 @@ Social backends angel aol appsfuel + beats behance belgium_eid bitbucket diff --git a/docs/intro.rst b/docs/intro.rst index 03335f31c..eb641a7e7 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -35,6 +35,7 @@ Several supported service by simple backends definition (easy to add new ones or extend current one): * Angel_ OAuth2 + * Beats_ OAuth2 * Behance_ OAuth2 * Bitbucket_ OAuth1 * Box_ OAuth2 @@ -112,6 +113,7 @@ section. .. _OAuth: http://oauth.net/ .. _myOpenID: https://www.myopenid.com/ .. _Angel: https://angel.co +.. _Beats: https://www.beats.com .. _Behance: https://www.behance.net .. _Bitbucket: https://bitbucket.org .. _Box: https://www.box.com diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index f76dc7756..3d262469f 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -123,6 +123,7 @@ 'social.backends.angel.AngelOAuth2', 'social.backends.aol.AOLOpenId', 'social.backends.appsfuel.AppsfuelOAuth2', + 'social.backends.beats.BeatsOAuth2', 'social.backends.behance.BehanceOAuth2', 'social.backends.belgiumeid.BelgiumEIDOpenId', 'social.backends.bitbucket.BitbucketOAuth', diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 0f9f4b6cf..dc091d3b0 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -6,6 +6,7 @@ Angel OAuth2
          AOL OpenId
          Appsfuel OAuth2
          +Beats OAuth2
          Behance OAuth2
          BelgiumEID OpenId
          Bitbucket OAuth1
          diff --git a/social/backends/beats.py b/social/backends/beats.py new file mode 100644 index 000000000..a2f996892 --- /dev/null +++ b/social/backends/beats.py @@ -0,0 +1,54 @@ +""" +Beats backend, docs at: + https://developer.beatsmusic.com/docs +""" +from re import sub + +import base64 + +from social.p3 import urlencode +from social.backends.oauth import BaseOAuth2 + +class BeatsOAuth2(BaseOAuth2): + name = 'beats' + SCOPE_SEPARATOR = ' ' + ID_KEY = 'user_context' + AUTHORIZATION_URL = 'https://partner.api.beatsmusic.com/v1/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://partner.api.beatsmusic.com/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False +# STATE_PARAMETER = False +# EXTRA_DATA = [ +# ('id', 'username'), +# ] + + def get_user_id(self, details, response): + return response["result"][BeatsOAuth2.ID_KEY] + + def auth_headers(self): + return { + 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( + ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) + )) + } + + def get_user_details(self, response): + """Return user details from Beats account""" + response = response["result"] + print response + fullname, first_name, last_name = self.get_user_names( + response.get('display_name') + ) + return {'username': response.get('id'), + 'email': response.get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + + return self.get_json( + 'https://partner.api.beatsmusic.com/v1/api/me', + headers={'Authorization': 'Bearer {0}'.format(access_token)} + ) \ No newline at end of file diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 3f091bf7a..3ea917ab1 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -354,6 +354,11 @@ def auth_complete(self, *args, **kwargs): except KeyError: raise AuthUnknownError(self) self.process_error(response) + + # mashery wraps in jsonrpc + if response.get('jsonrpc', None): + response = response.get('result', None) + return self.do_auth(response['access_token'], response=response, *args, **kwargs) From 1a6213c0ddef632b4c37dfcf267524b6cfa7dbf5 Mon Sep 17 00:00:00 2001 From: Ryan Choi Date: Sat, 17 May 2014 01:14:54 -0700 Subject: [PATCH 234/890] remove mashery stuff from oauth; constrain it to beats --- social/backends/beats.py | 26 ++++++++++++++++++++++++++ social/backends/oauth.py | 4 ---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/social/backends/beats.py b/social/backends/beats.py index a2f996892..8a2427e02 100644 --- a/social/backends/beats.py +++ b/social/backends/beats.py @@ -32,6 +32,32 @@ def auth_headers(self): )) } + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + self.process_error(self.data) + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + raise + except KeyError: + raise AuthUnknownError(self) + self.process_error(response) + + # mashery wraps in jsonrpc + if response.get('jsonrpc', None): + response = response.get('result', None) + + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) + def get_user_details(self, response): """Return user details from Beats account""" response = response["result"] diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 3ea917ab1..0ebc95735 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -355,10 +355,6 @@ def auth_complete(self, *args, **kwargs): raise AuthUnknownError(self) self.process_error(response) - # mashery wraps in jsonrpc - if response.get('jsonrpc', None): - response = response.get('result', None) - return self.do_auth(response['access_token'], response=response, *args, **kwargs) From 33e5dbd8fb185a106dca388a31da1bd723f5ae1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 17 May 2014 15:44:24 -0300 Subject: [PATCH 235/890] Fix title underline in docs --- docs/backends/mapmyfitness.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/mapmyfitness.rst b/docs/backends/mapmyfitness.rst index 1482d9667..6a5876eb8 100644 --- a/docs/backends/mapmyfitness.rst +++ b/docs/backends/mapmyfitness.rst @@ -1,5 +1,5 @@ MapMyFitness -========= +============ MapMyFitness uses OAuth v2 for authentication. From e35251878a88954cecf8e575eca27c63164b9f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 17 May 2014 16:08:23 -0300 Subject: [PATCH 236/890] Update google scopes, remove the soon to be deprecated ones. Fixes #273 --- social/backends/google.py | 25 +++++++----- social/tests/backends/test_google.py | 61 +++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/social/backends/google.py b/social/backends/google.py index e1a03574c..afb5caf81 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -18,13 +18,19 @@ def get_user_id(self, details, response): return details['email'] def get_user_details(self, response): - """Return user details from Orkut account""" - email = response.get('email', '') + """Return user details from Google API account""" + if response.get('emails'): + email = response['emails'][0]['value'] + elif response.get('email'): + email = response['email'] + else: + email = '' + names = response.get('name') or {} fullname, first_name, last_name = self.get_user_names( - response.get('name', ''), - response.get('given_name', ''), - response.get('family_name', '') + response.get('displayName', ''), + names.get('givenName', ''), + names.get('familyName', '') ) return {'username': email.split('@', 1)[0], 'email': email, @@ -37,7 +43,7 @@ class BaseGoogleOAuth2API(BaseGoogleAuth): def user_data(self, access_token, *args, **kwargs): """Return user data from Google API""" return self.get_json( - 'https://www.googleapis.com/oauth2/v1/userinfo', + 'https://www.googleapis.com/plus/v1/people/me', params={'access_token': access_token, 'alt': 'json'} ) @@ -51,8 +57,7 @@ class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile'] + DEFAULT_SCOPE = ['email', 'profile'] EXTRA_DATA = [ ('refresh_token', 'refresh_token', True), ('expires_in', 'expires'), @@ -74,9 +79,7 @@ class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['https://www.googleapis.com/auth/plus.login', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile'] + DEFAULT_SCOPE = ['plus.login', 'email'] EXTRA_DATA = [ ('id', 'user_id'), ('refresh_token', 'refresh_token', True), diff --git a/social/tests/backends/test_google.py b/social/tests/backends/test_google.py index ba63daa44..ab2fe50b8 100644 --- a/social/tests/backends/test_google.py +++ b/social/tests/backends/test_google.py @@ -13,25 +13,64 @@ class GoogleOAuth2Test(OAuth2Test): backend_path = 'social.backends.google.GoogleOAuth2' - user_data_url = 'https://www.googleapis.com/oauth2/v1/userinfo' + user_data_url = 'https://www.googleapis.com/plus/v1/people/me' expected_username = 'foo' access_token_body = json.dumps({ 'access_token': 'foobar', 'token_type': 'bearer' }) user_data_body = json.dumps({ - 'family_name': 'Bar', - 'name': 'Foo Bar', - 'picture': 'https://lh5.googleusercontent.com/-ui-GqpNh5Ms/' - 'AAAAAAAAAAI/AAAAAAAAAZw/a7puhHMO_fg/photo.jpg', - 'locale': 'en', + 'aboutMe': 'About me text', + 'cover': { + 'coverInfo': { + 'leftImageOffset': 0, + 'topImageOffset': 0 + }, + 'coverPhoto': { + 'height': 629, + 'url': 'https://lh5.googleusercontent.com/-ui-GqpNh5Ms/' + 'AAAAAAAAAAI/AAAAAAAAAZw/a7puhHMO_fg/photo.jpg', + 'width': 940 + }, + 'layout': 'banner' + }, + 'displayName': 'Foo Bar', + 'emails': [{ + 'type': 'account', + 'value': 'foo@bar.com' + }], + 'etag': '"e-tag string"', 'gender': 'male', - 'email': 'foo@bar.com', - 'birthday': '0000-01-22', - 'link': 'https://plus.google.com/101010101010101010101', - 'given_name': 'Foo', 'id': '101010101010101010101', - 'verified_email': True + 'image': { + 'url': 'https://lh5.googleusercontent.com/-ui-GqpNh5Ms/' + 'AAAAAAAAAAI/AAAAAAAAAZw/a7puhHMO_fg/photo.jpg', + }, + 'isPlusUser': True, + 'kind': 'plus#person', + 'language': 'en', + 'name': { + 'familyName': 'Bar', + 'givenName': 'Foo' + }, + 'objectType': 'person', + 'occupation': 'Software developer', + 'organizations': [{ + 'name': 'Org name', + 'primary': True, + 'type': 'school' + }], + 'placesLived': [{ + 'primary': True, + 'value': 'Anyplace' + }], + 'url': 'https://plus.google.com/101010101010101010101', + 'urls': [{ + 'label': 'http://foobar.com', + 'type': 'otherProfile', + 'value': 'http://foobar.com', + }], + 'verified': False }) def test_login(self): From feba4204480d6154134f9a4e21ea7d2660c8dab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 17 May 2014 18:04:47 -0300 Subject: [PATCH 237/890] Circumvent recursive import issue in admin. Fixes #269 --- docs/configuration/django.rst | 24 +++++++++++++++ examples/django_example/example/settings.py | 3 ++ social/apps/django_app/default/admin.py | 34 +++++++++++++-------- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index 0fe19664b..83fdff763 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -164,6 +164,30 @@ The redirect destination will get two ``GET`` parameters: Backend name that was used, if it was a valid backend. +Django Admin +------------ + +The default application (not the MongoEngine_ one) contains an ``admin.py`` +module that will be auto-discovered by the usual mechanism. + +But, by the nature of the application which depends on the existence of a user +model, it's easy to fall in a recursive import ordering making the application +fail to load. This happens because the admin module will build a set of fields +to populate the ``search_fields`` property to search for related users in the +administration UI, but this requires the user model to be retrieved which might +not be defined at that time. + +To avoid this issue define the following setting to circumvent the import +error:: + + SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['field1', 'field2'] + +For example:: + + SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['username', 'first_name', 'email'] + +The fields listed **must** be user models fields. + .. _MongoEngine: http://mongoengine.org .. _MongoEngine Django integration: http://mongoengine-odm.readthedocs.org/en/latest/django.html .. _django-social-auth: https://github.com/omab/django-social-auth diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 3eab0b4f3..c7d4f0bcc 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -225,6 +225,9 @@ 'social.pipeline.user.user_details' ) +# SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['first_name', 'last_name', 'email', +# 'username'] + try: from example.local_settings import * except ImportError: diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index 32f765957..2445c44a6 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -1,28 +1,38 @@ """Admin settings""" +from django.conf import settings from django.contrib import admin + +from social.utils import setting_name from social.apps.django_app.default.models import UserSocialAuth, Nonce, \ Association -_User = UserSocialAuth.user_model() - -username = getattr(_User, 'USERNAME_FIELD', None) or \ - hasattr(_User, 'username') and 'username' or \ - None -fieldnames = ('first_name', 'last_name', 'email', username) -all_names = _User._meta.get_all_field_names() -user_search_fields = ['user__' + name for name in fieldnames - if name and name in all_names] - - class UserSocialAuthOption(admin.ModelAdmin): """Social Auth user options""" list_display = ('id', 'user', 'provider', 'uid') - search_fields = user_search_fields list_filter = ('provider',) raw_id_fields = ('user',) list_select_related = True + def __init__(self, *args, **kwargs): + self.search_fields = self.get_search_fields() + super(UserSocialAuthOption, self).__init__(*args, **kwargs) + + def get_search_fields(self): + search_fields = getattr( + settings, setting_name('ADMIN_USER_SEARCH_FIELDS'), None + ) + if search_fields is None: + _User = UserSocialAuth.user_model() + username = getattr(_User, 'USERNAME_FIELD', None) or \ + hasattr(_User, 'username') and 'username' or \ + None + fieldnames = ('first_name', 'last_name', 'email', username) + all_names = _User._meta.get_all_field_names() + search_fields = [name for name in fieldnames + if name and name in all_names] + return ['user_' + name for name in search_fields] + class NonceOption(admin.ModelAdmin): """Nonce options""" From 709934abc558c82baf5eb93430444641fc302eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 17 May 2014 21:31:40 -0300 Subject: [PATCH 238/890] Example for ajax auth. Refs #272, #238 --- .../django_example/example/app/pipeline.py | 2 +- examples/django_example/example/app/views.py | 25 +++++++++- .../example/templates/home.html | 48 +++++++++++++++++++ examples/django_example/example/urls.py | 2 + 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/examples/django_example/example/app/pipeline.py b/examples/django_example/example/app/pipeline.py index 136f8b56e..a1cb2a127 100644 --- a/examples/django_example/example/app/pipeline.py +++ b/examples/django_example/example/app/pipeline.py @@ -5,7 +5,7 @@ @partial def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): - if user and user.email: + if kwargs.get('ajax') or user and user.email: return elif is_new and not details.get('email'): if strategy.session_get('saved_email'): diff --git a/examples/django_example/example/app/views.py b/examples/django_example/example/app/views.py index aca7c024a..52ea16ab7 100644 --- a/examples/django_example/example/app/views.py +++ b/examples/django_example/example/app/views.py @@ -1,10 +1,15 @@ +import json + from django.conf import settings +from django.http import HttpResponse, HttpResponseBadRequest from django.template import RequestContext from django.shortcuts import render_to_response, redirect from django.contrib.auth.decorators import login_required -from django.contrib.auth import logout as auth_logout +from django.contrib.auth import logout as auth_logout, login +from social.backends.oauth import BaseOAuth1, BaseOAuth2 from social.backends.google import GooglePlusAuth +from social.apps.django_app.utils import strategy def logout(request): @@ -49,3 +54,21 @@ def require_email(request): backend = request.session['partial_pipeline']['backend'] return redirect('social:complete', backend=backend) return render_to_response('email.html', RequestContext(request)) + + +@strategy('social:complete') +def ajax_auth(request, backend): + backend = request.strategy.backend + if isinstance(backend, BaseOAuth1): + token = { + 'oauth_token': request.REQUEST.get('access_token'), + 'oauth_token_secret': request.REQUEST.get('access_token_secret'), + } + elif isinstance(backend, BaseOAuth2): + token = request.REQUEST.get('access_token') + else: + raise HttpResponseBadRequest('Wrong backend type') + user = request.strategy.backend.do_auth(token, ajax=True) + login(request, user) + data = {'id': user.id, 'username': user.username} + return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index abb429430..b61a552a2 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -93,6 +93,30 @@ Persona +
          +
          + + + + + + + + +

          +
          + + +
          + {% if plus_id %}
          {% csrf_token %} @@ -158,4 +182,28 @@ } }; + + {% endblock %} diff --git a/examples/django_example/example/urls.py b/examples/django_example/example/urls.py index 2a562ab23..abe1a1396 100644 --- a/examples/django_example/example/urls.py +++ b/examples/django_example/example/urls.py @@ -12,6 +12,8 @@ url(r'^login/$', 'example.app.views.home'), url(r'^logout/$', 'example.app.views.logout'), url(r'^done/$', 'example.app.views.done', name='done'), + url(r'^ajax-auth/(?P[^/]+)/$', 'example.app.views.ajax_auth', + name='ajax-auth'), url(r'^email/$', 'example.app.views.require_email', name='require_email'), url(r'', include('social.apps.django_app.urls', namespace='social')) ) From dbee14b8d3aca26266864acc6831ad2a43d9a4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 17 May 2014 21:54:38 -0300 Subject: [PATCH 239/890] v0.1.24 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 99962032e..c1b25c2fd 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 1, 24) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From d5c928d9f586920c882479f1d9577e1533279c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 17 May 2014 21:56:03 -0300 Subject: [PATCH 240/890] v0.2.0-dev --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index c1b25c2fd..27782b198 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 24) -extra = '' +version = (0, 2, 0) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 8abfc48f854cd818d213f9efa3e9e87433db4dfa Mon Sep 17 00:00:00 2001 From: Hector Zhao Date: Tue, 20 May 2014 12:59:30 +0800 Subject: [PATCH 241/890] avoid updating default settings --- social/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/base.py b/social/backends/base.py index f24a83f0a..da1f8435f 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -185,7 +185,7 @@ def request_token_extra_arguments(self): def auth_extra_arguments(self): """Return extra arguments needed on auth process. The defaults can be overriden by GET parameters.""" - extra_arguments = self.setting('AUTH_EXTRA_ARGUMENTS', {}) + extra_arguments = self.setting('AUTH_EXTRA_ARGUMENTS', {}).copy() extra_arguments.update((key, self.data[key]) for key in extra_arguments if key in self.data) return extra_arguments From 9989479c37e560dfd781987136280392915c6644 Mon Sep 17 00:00:00 2001 From: Michael Godshall Date: Thu, 22 May 2014 14:37:11 -0700 Subject: [PATCH 242/890] Fixed Django 1.7 admin --- social/apps/django_app/default/admin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index 2445c44a6..3b33acd66 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -14,11 +14,7 @@ class UserSocialAuthOption(admin.ModelAdmin): raw_id_fields = ('user',) list_select_related = True - def __init__(self, *args, **kwargs): - self.search_fields = self.get_search_fields() - super(UserSocialAuthOption, self).__init__(*args, **kwargs) - - def get_search_fields(self): + def get_search_fields(self, request): search_fields = getattr( settings, setting_name('ADMIN_USER_SEARCH_FIELDS'), None ) From 2b49b564b12fc7d56e5a52c83ba5ab3a3cb55c9d Mon Sep 17 00:00:00 2001 From: Devin Sevilla Date: Sat, 24 May 2014 16:54:07 -0700 Subject: [PATCH 243/890] Rdio API methods use POST --- social/backends/rdio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/rdio.py b/social/backends/rdio.py index bce5fd648..e4af55164 100644 --- a/social/backends/rdio.py +++ b/social/backends/rdio.py @@ -65,7 +65,7 @@ class RdioOAuth2(BaseRdio, BaseOAuth2): ] def user_data(self, access_token, *args, **kwargs): - return self.get_json(RDIO_API, data={ + return self.get_json(RDIO_API, method='POST', data={ 'method': 'currentUser', 'extras': 'username,displayName,streamRegion', 'access_token': access_token From ee66183d84e8290f5b6cccd6f7a996a6e9dd17db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 26 May 2014 17:05:30 -0300 Subject: [PATCH 244/890] PEP8 --- social/backends/beats.py | 26 ++++++++++---------------- social/backends/oauth.py | 1 - social/backends/spotify.py | 10 ++-------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/social/backends/beats.py b/social/backends/beats.py index 8a2427e02..d5801eb8b 100644 --- a/social/backends/beats.py +++ b/social/backends/beats.py @@ -2,28 +2,26 @@ Beats backend, docs at: https://developer.beatsmusic.com/docs """ -from re import sub - import base64 -from social.p3 import urlencode +from requests import HTTPError + +from social.exceptions import AuthCanceled, AuthUnknownError from social.backends.oauth import BaseOAuth2 + class BeatsOAuth2(BaseOAuth2): name = 'beats' SCOPE_SEPARATOR = ' ' ID_KEY = 'user_context' - AUTHORIZATION_URL = 'https://partner.api.beatsmusic.com/v1/oauth2/authorize' + AUTHORIZATION_URL = \ + 'https://partner.api.beatsmusic.com/v1/oauth2/authorize' ACCESS_TOKEN_URL = 'https://partner.api.beatsmusic.com/oauth2/token' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False -# STATE_PARAMETER = False -# EXTRA_DATA = [ -# ('id', 'username'), -# ] def get_user_id(self, details, response): - return response["result"][BeatsOAuth2.ID_KEY] + return response['result'][BeatsOAuth2.ID_KEY] def auth_headers(self): return { @@ -50,18 +48,15 @@ def auth_complete(self, *args, **kwargs): except KeyError: raise AuthUnknownError(self) self.process_error(response) - # mashery wraps in jsonrpc if response.get('jsonrpc', None): response = response.get('result', None) - return self.do_auth(response['access_token'], response=response, *args, **kwargs) - + def get_user_details(self, response): """Return user details from Beats account""" - response = response["result"] - print response + response = response['result'] fullname, first_name, last_name = self.get_user_names( response.get('display_name') ) @@ -73,8 +68,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - return self.get_json( 'https://partner.api.beatsmusic.com/v1/api/me', headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) \ No newline at end of file + ) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 0ebc95735..3f091bf7a 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -354,7 +354,6 @@ def auth_complete(self, *args, **kwargs): except KeyError: raise AuthUnknownError(self) self.process_error(response) - return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/backends/spotify.py b/social/backends/spotify.py index 9bf97ebc1..e3c9be947 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -3,13 +3,11 @@ https://developer.spotify.com/spotify-web-api/ https://developer.spotify.com/spotify-web-api/authorization-guide/ """ -from re import sub - import base64 -from social.p3 import urlencode from social.backends.oauth import BaseOAuth2 + class SpotifyOAuth2(BaseOAuth2): name = 'spotify' SCOPE_SEPARATOR = ' ' @@ -19,9 +17,6 @@ class SpotifyOAuth2(BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False STATE_PARAMETER = False -# EXTRA_DATA = [ -# ('id', 'username'), -# ] def auth_headers(self): return { @@ -43,8 +38,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - return self.get_json( 'https://api.spotify.com/v1/me', headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) \ No newline at end of file + ) From b4d67ce235028cd30b5d7e058ce3f9a5087e10a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 26 May 2014 17:13:26 -0300 Subject: [PATCH 245/890] Make request parameter optional. Refs #286 --- social/apps/django_app/default/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index 3b33acd66..e1b787e69 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -14,7 +14,7 @@ class UserSocialAuthOption(admin.ModelAdmin): raw_id_fields = ('user',) list_select_related = True - def get_search_fields(self, request): + def get_search_fields(self, request=None): search_fields = getattr( settings, setting_name('ADMIN_USER_SEARCH_FIELDS'), None ) From 43dc9adab47dfeff99f9b5843e68141efb0dda03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 26 May 2014 18:16:29 -0300 Subject: [PATCH 246/890] Fix title underline --- docs/backends/beats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/beats.rst b/docs/backends/beats.rst index 15cadfdbc..27fece0dc 100644 --- a/docs/backends/beats.rst +++ b/docs/backends/beats.rst @@ -1,5 +1,5 @@ Beats -======= +===== Beats supports OAuth 2. From b8f0329c13b0e5454574c918a47f1ec8fda8f496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 26 May 2014 18:16:50 -0300 Subject: [PATCH 247/890] Document google scopes deprecation. Refs #285 --- docs/backends/google.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 27b13df69..45806b815 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -167,6 +167,35 @@ or:: depending on the backends in use. + +Scopes deprecation +------------------ + +Google is deprecating the full-url scopes from `Sept 1, 2014`_ in favor of +``Google+ API`` and the recently introduced shorter scopes names. But +``python-social-auth`` already introduced the scopes change at e3525187_ which +was released at ``v0.1.24``. + +But, to enable the new scopes the application requires ``Google+ API`` to be +enabled in the `Google console`_ dashboard, the change is quick and quite +simple, but if any developer desires to keep using the old scopes, it's +possible with the following settings:: + + # Google OAuth2 (google-oauth2) + SOCIAL_AUTH_GOOGLE_OAUTH2_IGNORE_DEFAULT_SCOPE = True + SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile' + ] + + # Google+ SignIn (google-plus) + SOCIAL_AUTH_GOOGLE_PLUS_IGNORE_DEFAULT_SCOPE = True + SOCIAL_AUTH_GOOGLE_PLUS_SCOPE = [ + 'https://www.googleapis.com/auth/plus.login', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile' + ] + .. _Google support: http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105 .. _Orkut API: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating .. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html @@ -180,3 +209,5 @@ depending on the backends in use. .. _whitelists: ../configuration/settings.html#whitelists .. _Google+ Sign In: https://developers.google.com/+/web/signin/ .. _Google console: https://code.google.com/apis/console +.. _Sept 1, 2014: https://developers.google.com/+/api/auth-migration#timetable +.. _e3525187: https://github.com/omab/python-social-auth/commit/e35251878a88954cecf8e575eca27c63164b9f67 From 4ab2a8c0a2434f361b97ea9b67947f4a4828a3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 28 May 2014 00:53:10 -0300 Subject: [PATCH 248/890] Remove eclipse settings from PR merge --- .settings/org.eclipse.core.resources.prefs | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .settings/org.eclipse.core.resources.prefs diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 8de73cc16..000000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -encoding//social/backends/spotify.py=utf-8 From 6f9a6ca5a0d2deb190137cd98f0986faacd6077a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 1 Jun 2014 05:04:53 -0300 Subject: [PATCH 249/890] Document steam player data saving --- docs/backends/steam.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/backends/steam.rst b/docs/backends/steam.rst index 87c1ffcbe..de38c382c 100644 --- a/docs/backends/steam.rst +++ b/docs/backends/steam.rst @@ -10,4 +10,10 @@ Configurable settings: SOCIAL_AUTH_STEAM_API_KEY = key + +- To save ``player`` data provided by Steam into ``extra_data``:: + + SOCIAL_AUTH_STEAM_EXTRA_DATA = ['player'] + + .. _Steam Dev: http://steamcommunity.com/dev/apikey From e693f2c48b24c4e959f07963f43635e321bf7860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 12:47:02 -0300 Subject: [PATCH 250/890] Fix pipeline example --- docs/pipeline.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 68bca16a4..5bba174a2 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -30,7 +30,7 @@ user instances and gathers basic data from providers. The default pipeline is composed by:: ( - 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_details', 'social.pipeline.social_auth.social_uid', 'social.pipeline.social_auth.auth_allowed', 'social.pipeline.social_auth.social_user', @@ -46,13 +46,16 @@ for example a pipeline that won't create users, just accept already registered ones would look like this:: SOCIAL_AUTH_PIPELINE = ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.auth_allowed', 'social.pipeline.social_auth.social_user', 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', 'social.pipeline.user.user_details' ) -Note that this assumes the user is already authenticated, and thus the ``user`` key +Note that this assumes the user is already authenticated, and thus the ``user`` key in the dict is populated. In cases where the authentication is purely external, a pipeline method must be provided that populates the ``user`` key. Example:: From f35bc82018df3f93181433b25902f57620cdc4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 15:00:07 -0300 Subject: [PATCH 251/890] Support deprecated and new Google API. Refs #292. Refs #285 --- docs/backends/google.rst | 7 ++++ social/backends/google.py | 58 ++++++++++++++++++++++------ social/tests/backends/test_google.py | 39 ++++++++++++++++++- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 45806b815..9eefb67fa 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -196,6 +196,13 @@ possible with the following settings:: 'https://www.googleapis.com/auth/userinfo.profile' ] +To ease the change, the old API and scopes is still supported by the +application, the new values are the default option but if having troubles +supporting them you can default to the old values by defining this setting:: + + SOCIAL_AUTH_GOOGLE_OAUTH2_USE_DEPRECATED_API = True + SOCIAL_AUTH_GOOGLE_PLUS_USE_DEPRECATED_API = True + .. _Google support: http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105 .. _Orkut API: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating .. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html diff --git a/social/backends/google.py b/social/backends/google.py index afb5caf81..16247b5e8 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -19,18 +19,27 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Google API account""" - if response.get('emails'): - email = response['emails'][0]['value'] - elif response.get('email'): + if 'email' in response: email = response['email'] + elif 'emails' in response: + email = response['emails'][0]['value'] else: email = '' - - names = response.get('name') or {} + if self.setting('USE_DEPRECATED_API', False): + name, given_name, family_name = ( + response.get('name', ''), + response.get('given_name', ''), + response.get('family_name', '') + ) + else: + names = response.get('name') or {} + name, given_name, family_name = ( + response.get('displayName', ''), + names.get('givenName', ''), + names.get('familyName', '') + ) fullname, first_name, last_name = self.get_user_names( - response.get('displayName', ''), - names.get('givenName', ''), - names.get('familyName', '') + name, given_name, family_name ) return {'username': email.split('@', 1)[0], 'email': email, @@ -40,12 +49,28 @@ def get_user_details(self, response): class BaseGoogleOAuth2API(BaseGoogleAuth): + def get_scope(self): + """Return list with needed access scope""" + scope = self.setting('SCOPE', []) + if not self.setting('IGNORE_DEFAULT_SCOPE', False): + default_scope = [] + if self.setting('USE_DEPRECATED_API', False): + default_scope = self.DEPRECATED_DEFAULT_SCOPE + else: + default_scope = self.DEFAULT_SCOPE + scope += default_scope or [] + return scope + def user_data(self, access_token, *args, **kwargs): """Return user data from Google API""" - return self.get_json( - 'https://www.googleapis.com/plus/v1/people/me', - params={'access_token': access_token, 'alt': 'json'} - ) + if self.setting('USE_DEPRECATED_API', False): + url = 'https://www.googleapis.com/oauth2/v1/userinfo' + else: + url = 'https://www.googleapis.com/plus/v1/people/me' + return self.get_json(url, params={ + 'access_token': access_token, + 'alt': 'json' + }) class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): @@ -58,6 +83,10 @@ class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' DEFAULT_SCOPE = ['email', 'profile'] + DEPRECATED_DEFAULT_SCOPE = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile' + ] EXTRA_DATA = [ ('refresh_token', 'refresh_token', True), ('expires_in', 'expires'), @@ -80,6 +109,11 @@ class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' DEFAULT_SCOPE = ['plus.login', 'email'] + DEPRECATED_DEFAULT_SCOPE = [ + 'https://www.googleapis.com/auth/plus.login', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile' + ] EXTRA_DATA = [ ('id', 'user_id'), ('refresh_token', 'refresh_token', True), diff --git a/social/tests/backends/test_google.py b/social/tests/backends/test_google.py index ab2fe50b8..bef2d088b 100644 --- a/social/tests/backends/test_google.py +++ b/social/tests/backends/test_google.py @@ -81,7 +81,44 @@ def test_partial_pipeline(self): def test_with_unique_user_id(self): self.strategy.set_settings({ - 'SOCIAL_AUTH_GOOGLE_OAUTH2_USE_UNIQUE_USER_ID': True + 'SOCIAL_AUTH_GOOGLE_OAUTH2_USE_UNIQUE_USER_ID': True, + }) + self.do_login() + + +class GoogleOAuth2DeprecatedAPITest(GoogleOAuth2Test): + user_data_url = 'https://www.googleapis.com/oauth2/v1/userinfo' + user_data_body = json.dumps({ + 'family_name': 'Bar', + 'name': 'Foo Bar', + 'picture': 'https://lh5.googleusercontent.com/-ui-GqpNh5Ms/' + 'AAAAAAAAAAI/AAAAAAAAAZw/a7puhHMO_fg/photo.jpg', + 'locale': 'en', + 'gender': 'male', + 'email': 'foo@bar.com', + 'birthday': '0000-01-22', + 'link': 'https://plus.google.com/101010101010101010101', + 'given_name': 'Foo', + 'id': '101010101010101010101', + 'verified_email': True + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GOOGLE_OAUTH2_USE_DEPRECATED_API': True + }) + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GOOGLE_OAUTH2_USE_DEPRECATED_API': True + }) + self.do_partial_pipeline() + + def test_with_unique_user_id(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GOOGLE_OAUTH2_USE_UNIQUE_USER_ID': True, + 'SOCIAL_AUTH_GOOGLE_OAUTH2_USE_DEPRECATED_API': True }) self.do_login() From 71e353d071ef9601be62b7d01530629322426c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 15:06:24 -0300 Subject: [PATCH 252/890] v0.1.25 --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 27782b198..94e5e0e21 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 0) -extra = '-dev' +version = (0, 1, 25) +extra = '' __version__ = '.'.join(map(str, version)) + extra From bb306e012de659dc4d0493c50cdbb5fc415c2ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 15:57:32 -0300 Subject: [PATCH 253/890] Fix google-plus scope, support server-side flow --- social/backends/google.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/social/backends/google.py b/social/backends/google.py index 16247b5e8..33428b476 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -104,11 +104,14 @@ class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): name = 'google-plus' REDIRECT_STATE = False STATE_PARAMETER = False + AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth' ACCESS_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' ACCESS_TOKEN_METHOD = 'POST' REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['plus.login', 'email'] + DEFAULT_SCOPE = [ + 'https://www.googleapis.com/auth/plus.login', + ] DEPRECATED_DEFAULT_SCOPE = [ 'https://www.googleapis.com/auth/plus.login', 'https://www.googleapis.com/auth/userinfo.email', @@ -124,18 +127,22 @@ class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): def auth_complete_params(self, state=None): params = super(GooglePlusAuth, self).auth_complete_params(state) - params['redirect_uri'] = 'postmessage' + if self.data.get('access_token'): + # Don't add postmessage if this is plain server-side workflow + params['redirect_uri'] = 'postmessage' return params def auth_complete(self, *args, **kwargs): - token = self.data.get('access_token') - if not token: - raise AuthMissingParameter(self, 'access_token') + if 'access_token' in self.data and not 'code' in self.data: + raise AuthMissingParameter(self, 'access_token or code') - self.process_error(self.get_json( - 'https://www.googleapis.com/oauth2/v1/tokeninfo', - params={'access_token': token} - )) + # Token won't be available in plain server-side workflow + token = self.data.get('access_token') + if token: + self.process_error(self.get_json( + 'https://www.googleapis.com/oauth2/v1/tokeninfo', + params={'access_token': token} + )) try: response = self.request_access_token( From 8a3e086697ddde7f70a74c5543730df8601e21d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 15:57:49 -0300 Subject: [PATCH 254/890] v0.1.26 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 94e5e0e21..e40784383 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 25) +version = (0, 1, 26) extra = '' __version__ = '.'.join(map(str, version)) + extra From 4b2e215652dbe725d9b57bbaf4e0a1a3a529e9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 17:32:09 -0300 Subject: [PATCH 255/890] Support MergeDict and MultiDict in partial cleanup. Refs #291 --- social/strategies/base.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/social/strategies/base.py b/social/strategies/base.py index 383e58b7f..f0dae3347 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -134,17 +134,23 @@ def partial_to_session(self, next, backend, request=None, *args, **kwargs): } or None } clean_kwargs.update(kwargs) + # Clean any MergeDict data type from the values - clean_kwargs.update((name, dict(value)) - for name, value in clean_kwargs.items() - if isinstance(value, dict)) + kwargs = {} + for name, value in clean_kwargs.items(): + # Check for class name to avoid importing Django MergeDict or + # Werkzeug MultiDict + if isinstance(value, dict) or \ + value.__class__.__name__ in ('MergeDict', 'MultiDict'): + value = dict(value) + if isinstance(value, self.SERIALIZABLE_TYPES): + kwargs[name] = self.to_session_value(value) + return { 'next': next, 'backend': backend.name, 'args': tuple(map(self.to_session_value, args)), - 'kwargs': dict((key, self.to_session_value(val)) - for key, val in clean_kwargs.items() - if isinstance(val, self.SERIALIZABLE_TYPES)) + 'kwargs': kwargs } def partial_from_session(self, session): From 7a21ae5626b73a6409f6f5c7ed58a1416adc55af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 26 May 2014 17:00:30 -0300 Subject: [PATCH 256/890] Refactor backend/strategy to avoid circular dependency --- examples/django_example/example/app/views.py | 11 ++- examples/webpy_example/app.py | 4 +- social/actions.py | 76 ++++++++++---------- social/apps/cherrypy_app/utils.py | 33 ++++----- social/apps/cherrypy_app/views.py | 17 ++--- social/apps/django_app/utils.py | 27 ++++--- social/apps/django_app/views.py | 24 +++---- social/apps/flask_app/fields.py | 11 --- social/apps/flask_app/models.py | 4 +- social/apps/flask_app/routes.py | 16 ++--- social/apps/flask_app/utils.py | 21 ++++-- social/apps/flask_me_app/routes.py | 16 ++--- social/apps/flask_me_app/utils.py | 21 ++++-- social/apps/pyramid_app/fields.py | 11 --- social/apps/pyramid_app/models.py | 4 +- social/apps/pyramid_app/utils.py | 24 ++++--- social/apps/pyramid_app/views.py | 16 ++--- social/apps/tornado_app/fields.py | 11 --- social/apps/tornado_app/handlers.py | 14 ++-- social/apps/tornado_app/models.py | 4 +- social/apps/tornado_app/utils.py | 20 +++--- social/apps/webpy_app/app.py | 18 ++--- social/apps/webpy_app/fields.py | 11 --- social/apps/webpy_app/models.py | 4 +- social/apps/webpy_app/utils.py | 26 +++---- social/backends/base.py | 17 ++++- social/backends/utils.py | 5 +- social/backends/vk.py | 2 +- social/pipeline/mail.py | 23 +++--- social/pipeline/social_auth.py | 48 ++++++------- social/storage/sqlalchemy_orm.py | 11 +++ social/strategies/base.py | 45 +++--------- social/strategies/cherrypy_strategy.py | 10 ++- social/strategies/django_strategy.py | 17 +++-- social/strategies/flask_strategy.py | 4 +- social/strategies/pyramid_strategy.py | 6 ++ social/strategies/tornado_strategy.py | 10 +-- social/strategies/utils.py | 14 +--- social/strategies/webpy_strategy.py | 13 ++-- social/tests/actions/actions.py | 23 +++--- social/tests/actions/test_disconnect.py | 8 +-- social/tests/backends/base.py | 5 +- social/tests/backends/open_id.py | 6 +- social/tests/backends/test_dummy.py | 7 +- social/tests/backends/test_google.py | 7 +- social/tests/strategy.py | 7 +- social/tests/test_pipeline.py | 4 +- social/utils.py | 17 ++--- 48 files changed, 362 insertions(+), 391 deletions(-) delete mode 100644 social/apps/flask_app/fields.py delete mode 100644 social/apps/pyramid_app/fields.py delete mode 100644 social/apps/tornado_app/fields.py delete mode 100644 social/apps/webpy_app/fields.py diff --git a/examples/django_example/example/app/views.py b/examples/django_example/example/app/views.py index 52ea16ab7..c2e5c7e7a 100644 --- a/examples/django_example/example/app/views.py +++ b/examples/django_example/example/app/views.py @@ -9,7 +9,7 @@ from social.backends.oauth import BaseOAuth1, BaseOAuth2 from social.backends.google import GooglePlusAuth -from social.apps.django_app.utils import strategy +from social.apps.django_app.utils import psa def logout(request): @@ -56,19 +56,18 @@ def require_email(request): return render_to_response('email.html', RequestContext(request)) -@strategy('social:complete') +@psa('social:complete') def ajax_auth(request, backend): - backend = request.strategy.backend - if isinstance(backend, BaseOAuth1): + if isinstance(request.backend, BaseOAuth1): token = { 'oauth_token': request.REQUEST.get('access_token'), 'oauth_token_secret': request.REQUEST.get('access_token_secret'), } - elif isinstance(backend, BaseOAuth2): + elif isinstance(request.backend, BaseOAuth2): token = request.REQUEST.get('access_token') else: raise HttpResponseBadRequest('Wrong backend type') - user = request.strategy.backend.do_auth(token, ajax=True) + user = request.backend.do_auth(token, ajax=True) login(request, user) data = {'id': user.id, 'username': user.username} return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index bafde9ec8..88d687fc3 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker from social.utils import setting_name -from social.apps.webpy_app.utils import strategy, backends +from social.apps.webpy_app.utils import psa, backends from social.apps.webpy_app import app as social_app import local_settings @@ -74,7 +74,7 @@ def GET(self): class done(social_app.BaseViewClass): - @strategy() + @psa() def GET(self): user = self.get_current_user() return render.done(user=user, backends=backends(user)) diff --git a/social/actions.py b/social/actions.py index 53a848693..1781cb216 100644 --- a/social/actions.py +++ b/social/actions.py @@ -3,54 +3,54 @@ user_is_active, partial_pipeline_data, setting_url -def do_auth(strategy, redirect_name='next'): +def do_auth(backend, redirect_name='next'): # Save any defined next value into session - data = strategy.request_data(merge=False) + data = backend.strategy.request_data(merge=False) # Save extra data into session. - for field_name in strategy.setting('FIELDS_STORED_IN_SESSION', []): + for field_name in backend.setting('FIELDS_STORED_IN_SESSION', []): if field_name in data: - strategy.session_set(field_name, data[field_name]) + backend.strategy.session_set(field_name, data[field_name]) if redirect_name in data: # Check and sanitize a user-defined GET/POST next field value redirect_uri = data[redirect_name] - if strategy.setting('SANITIZE_REDIRECTS', True): - redirect_uri = sanitize_redirect(strategy.request_host(), + if backend.setting('SANITIZE_REDIRECTS', True): + redirect_uri = sanitize_redirect(backend.strategy.request_host(), redirect_uri) - strategy.session_set( + backend.strategy.session_set( redirect_name, - redirect_uri or strategy.setting('LOGIN_REDIRECT_URL') + redirect_uri or backend.setting('LOGIN_REDIRECT_URL') ) - return strategy.start() + return backend.start() -def do_complete(strategy, login, user=None, redirect_name='next', +def do_complete(backend, login, user=None, redirect_name='next', *args, **kwargs): # pop redirect value before the session is trashed on login() - data = strategy.request_data() - redirect_value = strategy.session_get(redirect_name, '') or \ + data = backend.strategy.request_data() + redirect_value = backend.strategy.session_get(redirect_name, '') or \ data.get(redirect_name, '') is_authenticated = user_is_authenticated(user) user = is_authenticated and user or None - partial = partial_pipeline_data(strategy, user, *args, **kwargs) + partial = partial_pipeline_data(backend, user, *args, **kwargs) if partial: xargs, xkwargs = partial - user = strategy.continue_pipeline(*xargs, **xkwargs) + user = backend.continue_pipeline(*xargs, **xkwargs) else: - user = strategy.complete(user=user, request=strategy.request, - *args, **kwargs) + user = backend.complete(user=user, *args, **kwargs) - if user and not isinstance(user, strategy.storage.user.user_model()): + user_model = backend.strategy.storage.user.user_model() + if user and not isinstance(user, user_model): return user if is_authenticated: if not user: - url = setting_url(strategy, redirect_value, 'LOGIN_REDIRECT_URL') + url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL') else: - url = setting_url(strategy, redirect_value, + url = setting_url(backend, redirect_value, 'NEW_ASSOCIATION_REDIRECT_URL', 'LOGIN_REDIRECT_URL') elif user: @@ -58,53 +58,53 @@ def do_complete(strategy, login, user=None, redirect_name='next', # catch is_new/social_user in case login() resets the instance is_new = getattr(user, 'is_new', False) social_user = user.social_user - login(strategy, user, social_user) + login(backend, user, social_user) # store last login backend name in session - strategy.session_set('social_auth_last_login_backend', - social_user.provider) + backend.strategy.session_set('social_auth_last_login_backend', + social_user.provider) if is_new: - url = setting_url(strategy, + url = setting_url(backend, 'NEW_USER_REDIRECT_URL', redirect_value, 'LOGIN_REDIRECT_URL') else: - url = setting_url(strategy, redirect_value, + url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL') else: - url = setting_url(strategy, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', + url = setting_url(backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', 'LOGIN_URL') else: - url = setting_url(strategy, 'LOGIN_ERROR_URL', 'LOGIN_URL') + url = setting_url(backend, 'LOGIN_ERROR_URL', 'LOGIN_URL') if redirect_value and redirect_value != url: redirect_value = quote(redirect_value) url += ('?' in url and '&' or '?') + \ '{0}={1}'.format(redirect_name, redirect_value) - if strategy.setting('SANITIZE_REDIRECTS', True): - url = sanitize_redirect(strategy.request_host(), url) or \ - strategy.setting('LOGIN_REDIRECT_URL') - return strategy.redirect(url) + if backend.setting('SANITIZE_REDIRECTS', True): + url = sanitize_redirect(backend.strategy.request_host(), url) or \ + backend.setting('LOGIN_REDIRECT_URL') + return backend.strategy.redirect(url) -def do_disconnect(strategy, user, association_id=None, redirect_name='next', +def do_disconnect(backend, user, association_id=None, redirect_name='next', *args, **kwargs): - partial = partial_pipeline_data(strategy, user, *args, **kwargs) + partial = partial_pipeline_data(backend.strategy, user, *args, **kwargs) if partial: xargs, xkwargs = partial if association_id and not xkwargs.get('association_id'): xkwargs['association_id'] = association_id - response = strategy.disconnect(*xargs, **xkwargs) + response = backend.strategy.disconnect(*xargs, **xkwargs) else: - response = strategy.disconnect(user=user, + response = backend.strategy.disconnect(user=user, association_id=association_id, *args, **kwargs) if isinstance(response, dict): - response = strategy.redirect( - strategy.request_data().get(redirect_name, '') or - strategy.setting('DISCONNECT_REDIRECT_URL') or - strategy.setting('LOGIN_REDIRECT_URL') + response = backend.strategy.redirect( + backend.strategy.request_data().get(redirect_name, '') or + backend.setting('DISCONNECT_REDIRECT_URL') or + backend.setting('LOGIN_REDIRECT_URL') ) return response diff --git a/social/apps/cherrypy_app/utils.py b/social/apps/cherrypy_app/utils.py index 5196a5f9e..afdd1659c 100644 --- a/social/apps/cherrypy_app/utils.py +++ b/social/apps/cherrypy_app/utils.py @@ -4,7 +4,7 @@ from social.utils import setting_name, module_member from social.strategies.utils import get_strategy -from social.backends.utils import user_backends_data +from social.backends.utils import get_backend, user_backends_data DEFAULTS = { @@ -13,30 +13,27 @@ } -def get_helper(name, do_import=False): - config = cherrypy.config.get(setting_name(name), DEFAULTS.get(name, None)) - return do_import and module_member(config) or config +def get_helper(name): + return cherrypy.config.get(setting_name(name), DEFAULTS.get(name, None)) -def strategy(redirect_uri=None): +def load_backend(strategy, name, redirect_uri): + backends = get_helper('AUTHENTICATION_BACKENDS') + Backend = get_backend(backends, name) + return Backend(strategy=strategy, redirect_uri=redirect_uri) + + +def psa(redirect_uri=None): def decorator(func): @wraps(func) def wrapper(self, backend=None, *args, **kwargs): uri = redirect_uri - if uri and backend and '%(backend)s' in uri: uri = uri % {'backend': backend} - - backends = get_helper('AUTHENTICATION_BACKENDS') - strategy = get_helper('STRATEGY') - storage = get_helper('STORAGE') - self.strategy = get_strategy(backends, strategy, storage, - cherrypy.request, backend, - redirect_uri=uri, *args, **kwargs) - if backend: - return func(self, backend=backend, *args, **kwargs) - else: - return func(self, *args, **kwargs) + self.strategy = get_strategy(get_helper('STRATEGY'), + get_helper('STORAGE')) + self.backend = load_backend(self.strategy, backend, uri) + return func(self, backend, *args, **kwargs) return wrapper return decorator @@ -45,4 +42,4 @@ def backends(user): """Load Social Auth current user data to context under the key 'backends'. Will return the output of social.backends.utils.user_backends_data.""" return user_backends_data(user, get_helper('AUTHENTICATION_BACKENDS'), - get_helper('STORAGE', do_import=True)) + module_member(get_helper('STORAGE'))) diff --git a/social/apps/cherrypy_app/views.py b/social/apps/cherrypy_app/views.py index 899384e1a..940868f45 100644 --- a/social/apps/cherrypy_app/views.py +++ b/social/apps/cherrypy_app/views.py @@ -2,27 +2,28 @@ from social.utils import setting_name, module_member from social.actions import do_auth, do_complete, do_disconnect -from social.apps.cherrypy_app.utils import strategy +from social.apps.cherrypy_app.utils import psa class CherryPyPSAViews(object): @cherrypy.expose - @strategy('/complete/%(backend)s') + @psa('/complete/%(backend)s') def login(self, backend): - return do_auth(self.strategy) + return do_auth(self.backend) @cherrypy.expose - @strategy('/complete/%(backend)s') + @psa('/complete/%(backend)s') def complete(self, backend, *args, **kwargs): login = cherrypy.config.get(setting_name('LOGIN_METHOD')) do_login = module_member(login) if login else self.do_login user = getattr(cherrypy.request, 'user', None) - return do_complete(self.strategy, do_login, user=user, *args, **kwargs) + return do_complete(self.backend, do_login, user=user, *args, **kwargs) @cherrypy.expose + @psa() def disconnect(self, backend, association_id=None): user = getattr(cherrypy.request, 'user', None) - return do_disconnect(self.strategy, user, association_id) + return do_disconnect(self.backend, user, association_id) - def do_login(self, strategy, user, social_user): - strategy.session_set('user_id', user.id) + def do_login(self, backend, user, social_user): + backend.strategy.session_set('user_id', user.id) diff --git a/social/apps/django_app/utils.py b/social/apps/django_app/utils.py index 6d5697eda..b45b62e46 100644 --- a/social/apps/django_app/utils.py +++ b/social/apps/django_app/utils.py @@ -7,6 +7,7 @@ from social.utils import setting_name, module_member from social.exceptions import MissingBackend from social.strategies.utils import get_strategy +from social.backends.utils import get_backend BACKENDS = settings.AUTHENTICATION_BACKENDS @@ -18,11 +19,16 @@ Storage = module_member(STORAGE) -def load_strategy(*args, **kwargs): - return get_strategy(BACKENDS, STRATEGY, STORAGE, *args, **kwargs) +def load_strategy(request=None): + return get_strategy(STRATEGY, STORAGE, request) -def strategy(redirect_uri=None, load_strategy=load_strategy): +def load_backend(strategy, name, redirect_uri): + Backend = get_backend(BACKENDS, name) + return Backend(strategy, redirect_uri) + + +def psa(redirect_uri=None, load_strategy=load_strategy): def decorator(func): @wraps(func) def wrapper(request, backend, *args, **kwargs): @@ -30,18 +36,17 @@ def wrapper(request, backend, *args, **kwargs): if uri and not uri.startswith('/'): uri = reverse(redirect_uri, args=(backend,)) - try: - request.social_strategy = load_strategy( - request=request, backend=backend, - redirect_uri=uri, *args, **kwargs - ) - except MissingBackend: - raise Http404('Backend not found') - + request.social_strategy = load_strategy(request) # backward compatibility in attribute name, only if not already # defined if not hasattr(request, 'strategy'): request.strategy = request.social_strategy + + try: + request.backend = load_backend(request.social_strategy, + backend, uri) + except MissingBackend: + raise Http404('Backend not found') return func(request, backend, *args, **kwargs) return wrapper return decorator diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 585811459..34039f8d8 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -4,36 +4,36 @@ from django.views.decorators.http import require_POST from social.actions import do_auth, do_complete, do_disconnect -from social.apps.django_app.utils import strategy +from social.apps.django_app.utils import psa -@strategy('social:complete') +@psa('social:complete') def auth(request, backend): - return do_auth(request.social_strategy, redirect_name=REDIRECT_FIELD_NAME) + return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME) @csrf_exempt -@strategy('social:complete') +@psa('social:complete') def complete(request, backend, *args, **kwargs): """Authentication complete view, override this view if transaction management doesn't suit your needs.""" - return do_complete(request.social_strategy, _do_login, request.user, + return do_complete(request.backend, _do_login, request.user, redirect_name=REDIRECT_FIELD_NAME, *args, **kwargs) @login_required -@strategy() +@psa() @require_POST @csrf_protect def disconnect(request, backend, association_id=None): """Disconnects given backend from current logged in user.""" - return do_disconnect(request.social_strategy, request.user, association_id, + return do_disconnect(request.backend, request.user, association_id, redirect_name=REDIRECT_FIELD_NAME) -def _do_login(strategy, user, social_user): - login(strategy.request, user) - if strategy.setting('SESSION_EXPIRATION', True): +def _do_login(backend, user, social_user): + login(backend.strategy.request, user) + if backend.setting('SESSION_EXPIRATION', True): # Set session expiration date if present and not disabled # by setting. Use last social-auth instance for current # provider, users can associate several accounts with @@ -41,9 +41,9 @@ def _do_login(strategy, user, social_user): expiration = social_user.expiration_datetime() if expiration: try: - strategy.request.session.set_expiry( + backend.strategy.request.session.set_expiry( expiration.seconds + expiration.days * 86400 ) except OverflowError: # Handle django time zone overflow - strategy.request.session.set_expiry(None) + backend.strategy.request.session.set_expiry(None) diff --git a/social/apps/flask_app/fields.py b/social/apps/flask_app/fields.py deleted file mode 100644 index 7af09e749..000000000 --- a/social/apps/flask_app/fields.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -from sqlalchemy.types import PickleType, Text - - -class JSONType(PickleType): - impl = Text - - def __init__(self, *args, **kwargs): - kwargs['pickler'] = json - super(JSONType, self).__init__(*args, **kwargs) diff --git a/social/apps/flask_app/models.py b/social/apps/flask_app/models.py index a89d3d1f3..422474dfc 100644 --- a/social/apps/flask_app/models.py +++ b/social/apps/flask_app/models.py @@ -8,8 +8,8 @@ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage -from social.apps.flask_app.fields import JSONType + BaseSQLAlchemyStorage, \ + JSONType class FlaskStorage(BaseSQLAlchemyStorage): diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index d12e28358..b56dd0d04 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -2,24 +2,24 @@ from flask.ext.login import login_required, login_user from social.actions import do_auth, do_complete, do_disconnect -from social.apps.flask_app.utils import strategy +from social.apps.flask_app.utils import psa social_auth = Blueprint('social', __name__) @social_auth.route('/login//', methods=('GET', 'POST')) -@strategy('social.complete') +@psa('social.complete') def auth(backend): - return do_auth(g.strategy) + return do_auth(g.backend) @social_auth.route('/complete//', methods=('GET', 'POST')) -@strategy('social.complete') +@psa('social.complete') def complete(backend, *args, **kwargs): """Authentication complete view, override this view if transaction management doesn't suit your needs.""" - return do_complete(g.strategy, login=do_login, user=g.user, + return do_complete(g.backend, login=do_login, user=g.user, *args, **kwargs) @@ -27,13 +27,13 @@ def complete(backend, *args, **kwargs): @social_auth.route('/disconnect///', methods=('POST',)) @login_required -@strategy() +@psa() def disconnect(backend, association_id=None): """Disconnects given backend from current logged in user.""" - return do_disconnect(g.strategy, g.user, association_id) + return do_disconnect(g.backend, g.user, association_id) -def do_login(strategy, user, social_user): +def do_login(backend, user, social_user): return login_user(user, remember=request.cookies.get('remember') or request.args.get('remember') or request.form.get('remember') or False) diff --git a/social/apps/flask_app/utils.py b/social/apps/flask_app/utils.py index f02514e24..fa424d77f 100644 --- a/social/apps/flask_app/utils.py +++ b/social/apps/flask_app/utils.py @@ -1,9 +1,10 @@ from functools import wraps -from flask import current_app, url_for, g, request +from flask import current_app, url_for, g from social.utils import module_member, setting_name from social.strategies.utils import get_strategy +from social.backends.utils import get_backend DEFAULTS = { @@ -18,22 +19,28 @@ def get_helper(name, do_import=False): return do_import and module_member(config) or config -def load_strategy(*args, **kwargs): - backends = get_helper('AUTHENTICATION_BACKENDS') +def load_strategy(): strategy = get_helper('STRATEGY') storage = get_helper('STORAGE') - return get_strategy(backends, strategy, storage, *args, **kwargs) + return get_strategy(strategy, storage) + + +def load_backend(strategy, name, redirect_uri): + backends = get_helper('AUTHENTICATION_BACKENDS') + Backend = get_backend(backends, name) + return Backend(strategy=strategy, redirect_uri=redirect_uri) -def strategy(redirect_uri=None): +def psa(redirect_uri=None): def decorator(func): @wraps(func) def wrapper(backend, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = url_for(uri, backend=backend) - g.strategy = load_strategy(request=request, backend=backend, - redirect_uri=uri, *args, **kwargs) + g.strategy = load_strategy() + g.backend = load_backend(g.strategy, backend, redirect_uri=uri, + *args, **kwargs) return func(backend, *args, **kwargs) return wrapper return decorator diff --git a/social/apps/flask_me_app/routes.py b/social/apps/flask_me_app/routes.py index 65ad645e6..41af2c383 100644 --- a/social/apps/flask_me_app/routes.py +++ b/social/apps/flask_me_app/routes.py @@ -2,24 +2,24 @@ from flask.ext.login import login_required, login_user from social.actions import do_auth, do_complete, do_disconnect -from social.apps.flask_me_app.utils import strategy +from social.apps.flask_me_app.utils import psa social_auth = Blueprint('social', __name__) @social_auth.route('/login//', methods=('GET', 'POST')) -@strategy('social.complete') +@psa('social.complete') def auth(backend): - return do_auth(g.strategy) + return do_auth(g.backend) @social_auth.route('/complete//', methods=('GET', 'POST')) -@strategy('social.complete') +@psa('social.complete') def complete(backend, *args, **kwargs): """Authentication complete view, override this view if transaction management doesn't suit your needs.""" - return do_complete(g.strategy, login=do_login, user=g.user, + return do_complete(g.backend, login=do_login, user=g.user, *args, **kwargs) @@ -27,13 +27,13 @@ def complete(backend, *args, **kwargs): @social_auth.route('/disconnect///', methods=('POST',)) @login_required -@strategy() +@psa() def disconnect(backend, association_id=None): """Disconnects given backend from current logged in user.""" - return do_disconnect(g.strategy, g.user, association_id) + return do_disconnect(g.backend, g.user, association_id) -def do_login(strategy, user, social_user): +def do_login(backend, user, social_user): return login_user(user, remember=request.cookies.get('remember') or request.args.get('remember') or request.form.get('remember') or False) diff --git a/social/apps/flask_me_app/utils.py b/social/apps/flask_me_app/utils.py index c0fd8cbf0..1a86b00f5 100644 --- a/social/apps/flask_me_app/utils.py +++ b/social/apps/flask_me_app/utils.py @@ -1,9 +1,10 @@ from functools import wraps -from flask import current_app, url_for, g, request +from flask import current_app, url_for, g from social.utils import module_member, setting_name from social.strategies.utils import get_strategy +from social.backends.utils import get_backend DEFAULTS = { @@ -18,22 +19,28 @@ def get_helper(name, do_import=False): return do_import and module_member(config) or config -def load_strategy(*args, **kwargs): - backends = get_helper('AUTHENTICATION_BACKENDS') +def load_strategy(): strategy = get_helper('STRATEGY') storage = get_helper('STORAGE') - return get_strategy(backends, strategy, storage, *args, **kwargs) + return get_strategy(strategy, storage) + + +def load_backend(strategy, name, redirect_uri): + backends = get_helper('AUTHENTICATION_BACKENDS') + Backend = get_backend(backends, name) + return Backend(strategy=strategy, redirect_uri=redirect_uri) -def strategy(redirect_uri=None): +def psa(redirect_uri=None): def decorator(func): @wraps(func) def wrapper(backend, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = url_for(uri, backend=backend) - g.strategy = load_strategy(request=request, backend=backend, - redirect_uri=uri, *args, **kwargs) + g.strategy = load_strategy() + g.backend = load_backend(g.strategy, backend, redirect_uri=uri, + *args, **kwargs) return func(backend, *args, **kwargs) return wrapper return decorator diff --git a/social/apps/pyramid_app/fields.py b/social/apps/pyramid_app/fields.py deleted file mode 100644 index 7af09e749..000000000 --- a/social/apps/pyramid_app/fields.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -from sqlalchemy.types import PickleType, Text - - -class JSONType(PickleType): - impl = Text - - def __init__(self, *args, **kwargs): - kwargs['pickler'] = json - super(JSONType, self).__init__(*args, **kwargs) diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py index f946375f6..bcd54939f 100644 --- a/social/apps/pyramid_app/models.py +++ b/social/apps/pyramid_app/models.py @@ -8,8 +8,8 @@ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage -from social.apps.pyramid_app.fields import JSONType + BaseSQLAlchemyStorage, \ + JSONType class PyramidStorage(BaseSQLAlchemyStorage): diff --git a/social/apps/pyramid_app/utils.py b/social/apps/pyramid_app/utils.py index 37659b391..64d3dc7ef 100644 --- a/social/apps/pyramid_app/utils.py +++ b/social/apps/pyramid_app/utils.py @@ -5,7 +5,7 @@ from social.utils import setting_name, module_member from social.strategies.utils import get_strategy -from social.backends.utils import user_backends_data +from social.backends.utils import get_backend, user_backends_data DEFAULTS = { @@ -19,14 +19,17 @@ def get_helper(name): return settings.get(setting_name(name), DEFAULTS.get(name, None)) -def load_strategy(*args, **kwargs): +def load_strategy(): + return get_strategy(get_helper('STRATEGY'), get_helper('STORAGE')) + + +def load_backend(strategy, name, redirect_uri): backends = get_helper('AUTHENTICATION_BACKENDS') - strategy = get_helper('STRATEGY') - storage = get_helper('STORAGE') - return get_strategy(backends, strategy, storage, *args, **kwargs) + Backend = get_backend(backends, name) + return Backend(strategy=strategy, redirect_uri=redirect_uri) -def strategy(redirect_uri=None): +def psa(redirect_uri=None): def decorator(func): @wraps(func) def wrapper(request, *args, **kwargs): @@ -37,10 +40,9 @@ def wrapper(request, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = request.route_url(uri, backend=backend) - request.strategy = load_strategy( - backend=backend, redirect_uri=uri, request=request, - *args, **kwargs - ) + + request.strategy = load_strategy() + request.backend = load_backend(request.strategy, backend, uri) return func(request, *args, **kwargs) return wrapper return decorator @@ -50,7 +52,7 @@ def login_required(func): @wraps(func) def wrapper(request, *args, **kwargs): is_logged_in = module_member( - request.strategy.setting('LOGGEDIN_FUNCTION') + request.backend.setting('LOGGEDIN_FUNCTION') ) if not is_logged_in(request): raise HTTPForbidden('Not authorized user') diff --git a/social/apps/pyramid_app/views.py b/social/apps/pyramid_app/views.py index 09b298c8f..38ee4fa33 100644 --- a/social/apps/pyramid_app/views.py +++ b/social/apps/pyramid_app/views.py @@ -2,29 +2,29 @@ from social.utils import module_member from social.actions import do_auth, do_complete, do_disconnect -from social.apps.pyramid_app.utils import strategy, login_required +from social.apps.pyramid_app.utils import psa, login_required @view_config(route_name='social.auth', request_method='GET') -@strategy('social.complete') +@psa('social.complete') def auth(request): - return do_auth(request.strategy, redirect_name='next') + return do_auth(request.backend, redirect_name='next') @view_config(route_name='social.complete', request_method=('GET', 'POST')) -@strategy('social.complete') +@psa('social.complete') def complete(request, *args, **kwargs): - do_login = module_member(request.strategy.setting('LOGIN_FUNCTION')) - return do_complete(request.strategy, do_login, request.user, + do_login = module_member(request.backend.setting('LOGIN_FUNCTION')) + return do_complete(request.backend, do_login, request.user, redirect_name='next', *args, **kwargs) @view_config(route_name='social.disconnect', request_method=('POST',)) @view_config(route_name='social.disconnect_association', request_method=('POST',)) -@strategy() +@psa() @login_required def disconnect(request): - return do_disconnect(request.strategy, request.user, + return do_disconnect(request.backend, request.user, request.matchdict.get('association_id'), redirect_name='next') diff --git a/social/apps/tornado_app/fields.py b/social/apps/tornado_app/fields.py deleted file mode 100644 index 7af09e749..000000000 --- a/social/apps/tornado_app/fields.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -from sqlalchemy.types import PickleType, Text - - -class JSONType(PickleType): - impl = Text - - def __init__(self, *args, **kwargs): - kwargs['pickler'] = json - super(JSONType, self).__init__(*args, **kwargs) diff --git a/social/apps/tornado_app/handlers.py b/social/apps/tornado_app/handlers.py index f6769272b..f31869692 100644 --- a/social/apps/tornado_app/handlers.py +++ b/social/apps/tornado_app/handlers.py @@ -1,6 +1,6 @@ from tornado.web import RequestHandler -from social.apps.tornado_app.utils import strategy +from social.apps.tornado_app.utils import psa from social.actions import do_auth, do_complete, do_disconnect @@ -11,7 +11,7 @@ def user_id(self): def get_current_user(self): user_id = self.user_id() if user_id: - return self.strategy.get_user(int(user_id)) + return self.backend.strategy.get_user(int(user_id)) def login_user(self, user): self.set_secure_cookie('user_id', str(user.id)) @@ -24,9 +24,9 @@ def get(self, backend): def post(self, backend): self._auth(backend) - @strategy('complete') + @psa('complete') def _auth(self, backend): - do_auth(self.strategy) + do_auth(self.backend) class CompleteHandler(BaseHandler): @@ -36,11 +36,11 @@ def get(self, backend): def post(self, backend): self._complete(backend) - @strategy('complete') + @psa('complete') def _complete(self, backend): do_complete( - self.strategy, - login=lambda strategy, user, social_user: self.login_user(user), + self.backend, + login=lambda backend, user, social_user: self.login_user(user), user=self.get_current_user() ) diff --git a/social/apps/tornado_app/models.py b/social/apps/tornado_app/models.py index 831e5ca36..40338066b 100644 --- a/social/apps/tornado_app/models.py +++ b/social/apps/tornado_app/models.py @@ -8,8 +8,8 @@ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage -from social.apps.tornado_app.fields import JSONType + BaseSQLAlchemyStorage, \ + JSONType class TornadoStorage(BaseSQLAlchemyStorage): diff --git a/social/apps/tornado_app/utils.py b/social/apps/tornado_app/utils.py index 0b423c3af..6026a44d9 100644 --- a/social/apps/tornado_app/utils.py +++ b/social/apps/tornado_app/utils.py @@ -2,6 +2,7 @@ from social.utils import setting_name from social.strategies.utils import get_strategy +from social.backends.utils import get_backend DEFAULTS = { @@ -15,24 +16,27 @@ def get_helper(request_handler, name): DEFAULTS.get(name, None)) -def load_strategy(request_handler, *args, **kwargs): - backends = get_helper(request_handler, 'AUTHENTICATION_BACKENDS') +def load_strategy(request_handler): strategy = get_helper(request_handler, 'STRATEGY') storage = get_helper(request_handler, 'STORAGE') - return get_strategy(backends, strategy, storage, request_handler.request, - request_handler=request_handler, *args, **kwargs) + return get_strategy(strategy, storage, request_handler) + + +def load_backend(request_handler, strategy, name, redirect_uri): + backends = get_helper(request_handler, 'AUTHENTICATION_BACKENDS') + Backend = get_backend(backends, name) + return Backend(strategy, redirect_uri) -def strategy(redirect_uri=None): +def psa(redirect_uri=None): def decorator(func): @wraps(func) def wrapper(self, backend, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = self.reverse_url(uri, backend) - self.strategy = load_strategy(self, - backend=backend, - redirect_uri=uri, *args, **kwargs) + self.strategy = load_strategy(self) + self.backend = load_backend(self, self.strategy, backend, uri) return func(self, backend, *args, **kwargs) return wrapper return decorator diff --git a/social/apps/webpy_app/app.py b/social/apps/webpy_app/app.py index 3135fb2c4..4cfe26cc7 100644 --- a/social/apps/webpy_app/app.py +++ b/social/apps/webpy_app/app.py @@ -1,7 +1,7 @@ import web from social.actions import do_auth, do_complete, do_disconnect -from social.apps.webpy_app.utils import strategy +from social.apps.webpy_app.utils import psa urls = ( @@ -22,7 +22,7 @@ def __init__(self, *args, **kwargs): def get_current_user(self): if not hasattr(self, '_user'): if self.session.get('logged_in'): - self._user = self.strategy.get_user( + self._user = self.backend.strategy.get_user( self.session.get('user_id') ) else: @@ -41,9 +41,9 @@ def GET(self, backend): def POST(self, backend): return self._auth(backend) - @strategy('/complete/%(backend)s/') + @psa('/complete/%(backend)s/') def _auth(self, backend): - return do_auth(self.strategy) + return do_auth(self.backend) class complete(BaseViewClass): @@ -53,19 +53,19 @@ def GET(self, backend, *args, **kwargs): def POST(self, backend, *args, **kwargs): return self._complete(backend, *args, **kwargs) - @strategy('/complete/%(backend)s/') + @psa('/complete/%(backend)s/') def _complete(self, backend, *args, **kwargs): return do_complete( - self.strategy, - login=lambda strat, user, social_user: self.login_user(user), + self.backend, + login=lambda backend, user, social_user: self.login_user(user), user=self.get_current_user(), *args, **kwargs ) class disconnect(BaseViewClass): - @strategy() + @psa() def POST(self, backend, association_id=None): - return do_disconnect(self.strategy, self.get_current_user(), + return do_disconnect(self.backend, self.get_current_user(), association_id) diff --git a/social/apps/webpy_app/fields.py b/social/apps/webpy_app/fields.py deleted file mode 100644 index 7af09e749..000000000 --- a/social/apps/webpy_app/fields.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -from sqlalchemy.types import PickleType, Text - - -class JSONType(PickleType): - impl = Text - - def __init__(self, *args, **kwargs): - kwargs['pickler'] = json - super(JSONType, self).__init__(*args, **kwargs) diff --git a/social/apps/webpy_app/models.py b/social/apps/webpy_app/models.py index 71be9975a..adfb3ce4c 100644 --- a/social/apps/webpy_app/models.py +++ b/social/apps/webpy_app/models.py @@ -11,8 +11,8 @@ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage -from social.apps.webpy_app.fields import JSONType + BaseSQLAlchemyStorage, \ + JSONType SocialBase = declarative_base() diff --git a/social/apps/webpy_app/utils.py b/social/apps/webpy_app/utils.py index 629188c8d..023bf7dfd 100644 --- a/social/apps/webpy_app/utils.py +++ b/social/apps/webpy_app/utils.py @@ -3,7 +3,7 @@ from functools import wraps from social.utils import setting_name, module_member -from social.backends.utils import user_backends_data +from social.backends.utils import get_backend, user_backends_data from social.strategies.utils import get_strategy @@ -19,26 +19,26 @@ def get_helper(name, do_import=False): return do_import and module_member(config) or config -def load_strategy(*args, **kwargs): +def load_strategy(): + return get_strategy(get_helper('STRATEGY'), get_helper('STORAGE')) + + +def load_backend(strategy, name, redirect_uri): backends = get_helper('AUTHENTICATION_BACKENDS') - strategy = get_helper('STRATEGY') - storage = get_helper('STORAGE') - return get_strategy(backends, strategy, storage, *args, **kwargs) + Backend = get_backend(backends, name) + return Backend(strategy, redirect_uri) -def strategy(redirect_uri=None): +def psa(redirect_uri=None): def decorator(func): @wraps(func) - def wrapper(self, backend=None, *args, **kwargs): + def wrapper(self, backend, *args, **kwargs): uri = redirect_uri if uri and backend and '%(backend)s' in uri: uri = uri % {'backend': backend} - self.strategy = load_strategy(request=web.ctx, backend=backend, - redirect_uri=uri, *args, **kwargs) - if backend: - return func(self, backend=backend, *args, **kwargs) - else: - return func(self, *args, **kwargs) + self.strategy = load_strategy() + self.backend = load_backend(self.strategy, backend, uri) + return func(self, backend=backend, *args, **kwargs) return wrapper return decorator diff --git a/social/backends/base.py b/social/backends/base.py index da1f8435f..d334970f9 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -13,7 +13,7 @@ class BaseAuth(object): EXTRA_DATA = None REQUIRES_EMAIL_VALIDATION = False - def __init__(self, strategy=None, redirect_uri=None, *args, **kwargs): + def __init__(self, strategy=None, redirect_uri=None): self.strategy = strategy self.redirect_uri = redirect_uri self.data = {} @@ -27,6 +27,17 @@ def setting(self, name, default=None): """Return setting value from strategy""" return self.strategy.setting(name, default=default, backend=self) + def start(self): + # Clean any partial pipeline info before starting the process + self.strategy.clean_partial_pipeline() + if self.uses_redirect(): + return self.strategy.redirect(self.auth_url()) + else: + return self.strategy.html(self.auth_html()) + + def complete(self, *args, **kwargs): + return self.auth_complete(*args, **kwargs) + def auth_url(self): """Must return redirect URL to auth provider""" raise NotImplementedError('Implement in subclass') @@ -90,7 +101,7 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): out = kwargs.copy() out.setdefault('strategy', self.strategy) out.setdefault('backend', out.pop(self.name, None) or self) - out.setdefault('request', self.strategy.request) + out.setdefault('request', self.strategy.request_data()) for idx, name in enumerate(pipeline): out['pipeline_index'] = pipeline_index + idx @@ -176,7 +187,7 @@ def get_user(self, user_id): def continue_pipeline(self, *args, **kwargs): """Continue previous halted pipeline""" kwargs.update({'backend': self}) - return self.strategy.authenticate(*args, **kwargs) + return self.authenticate(*args, **kwargs) def request_token_extra_arguments(self): """Return extra arguments needed on request-token process""" diff --git a/social/backends/utils.py b/social/backends/utils.py index 43502f9f4..7650e31e6 100644 --- a/social/backends/utils.py +++ b/social/backends/utils.py @@ -1,5 +1,6 @@ -from social.utils import module_member, user_is_authenticated +from social.exceptions import MissingBackend from social.backends.base import BaseAuth +from social.utils import module_member, user_is_authenticated # Cache for discovered backends. @@ -51,7 +52,7 @@ def get_backend(backends, name): try: return BACKENDSCACHE[name] except KeyError: - return None + raise MissingBackend(name) def user_backends_data(user, backends, storage): diff --git a/social/backends/vk.py b/social/backends/vk.py index 56a3ae547..1a94b5874 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -171,7 +171,7 @@ def auth_complete(self, *args, **kwargs): auth_data = { 'auth': self, 'backend': self, - 'request': self.strategy.request, + 'request': self.strategy.request_data(), 'response': { 'user_id': user_id, } diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index a8677210c..ac191718c 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -3,17 +3,20 @@ @partial -def mail_validation(strategy, details, *args, **kwargs): - requires_validation = strategy.backend.REQUIRES_EMAIL_VALIDATION or \ - strategy.setting('FORCE_EMAIL_VALIDATION', False) +def mail_validation(backend, details, *args, **kwargs): + requires_validation = backend.REQUIRES_EMAIL_VALIDATION or \ + backend.setting('FORCE_EMAIL_VALIDATION', False) if requires_validation and details.get('email'): - data = strategy.request_data() + data = backend.strategy.request_data() if 'verification_code' in data: - strategy.session_pop('email_validation_address') - if not strategy.validate_email(details['email'], + backend.strategy.session_pop('email_validation_address') + if not backend.strategy.validate_email(details['email'], data['verification_code']): - raise InvalidEmail(strategy.backend) + raise InvalidEmail(backend) else: - strategy.send_email_validation(details['email']) - strategy.session_set('email_validation_address', details['email']) - return strategy.redirect(strategy.setting('EMAIL_VALIDATION_URL')) + backend.strategy.send_email_validation(details['email']) + backend.strategy.session_set('email_validation_address', + details['email']) + return backend.strategy.redirect( + backend.strategy.setting('EMAIL_VALIDATION_URL') + ) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 6e317e6dc..419153d7a 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -2,26 +2,26 @@ AuthForbidden -def social_details(strategy, response, *args, **kwargs): - return {'details': strategy.backend.get_user_details(response)} +def social_details(backend, response, *args, **kwargs): + return {'details': backend.get_user_details(response)} -def social_uid(strategy, details, response, *args, **kwargs): - return {'uid': strategy.backend.get_user_id(details, response)} +def social_uid(backend, details, response, *args, **kwargs): + return {'uid': backend.get_user_id(details, response)} -def auth_allowed(strategy, details, response, *args, **kwargs): - if not strategy.backend.auth_allowed(response, details): - raise AuthForbidden(strategy.backend) +def auth_allowed(backend, details, response, *args, **kwargs): + if not backend.auth_allowed(response, details): + raise AuthForbidden(backend) -def social_user(strategy, uid, user=None, *args, **kwargs): - provider = strategy.backend.name - social = strategy.storage.user.get_social_auth(provider, uid) +def social_user(backend, uid, user=None, *args, **kwargs): + provider = backend.name + social = backend.strategy.storage.user.get_social_auth(provider, uid) if social: if user and social.user != user: msg = 'This {0} account is already in use.'.format(provider) - raise AuthAlreadyAssociated(strategy.backend, msg) + raise AuthAlreadyAssociated(backend, msg) elif not user: user = social.user return {'social': social, @@ -30,26 +30,26 @@ def social_user(strategy, uid, user=None, *args, **kwargs): 'new_association': False} -def associate_user(strategy, uid, user=None, social=None, *args, **kwargs): +def associate_user(backend, uid, user=None, social=None, *args, **kwargs): if user and not social: try: - social = strategy.storage.user.create_social_auth( - user, uid, strategy.backend.name + social = backend.strategy.storage.user.create_social_auth( + user, uid, backend.name ) except Exception as err: - if not strategy.storage.is_integrity_error(err): + if not backend.strategy.storage.is_integrity_error(err): raise # Protect for possible race condition, those bastard with FTL # clicking capabilities, check issue #131: # https://github.com/omab/django-social-auth/issues/131 - return social_user(strategy, uid, user, *args, **kwargs) + return social_user(backend.strategy, uid, user, *args, **kwargs) else: return {'social': social, 'user': social.user, 'new_association': True} -def associate_by_email(strategy, details, user=None, *args, **kwargs): +def associate_by_email(backend, details, user=None, *args, **kwargs): """ Associate current auth with a user with the same email address in the DB. @@ -67,23 +67,21 @@ def associate_by_email(strategy, details, user=None, *args, **kwargs): # Try to associate accounts registered with the same email address, # only if it's a single object. AuthException is raised if multiple # objects are returned. - users = list(strategy.storage.user.get_users_by_email(email)) + users = list(backend.strategy.storage.user.get_users_by_email(email)) if len(users) == 0: return None elif len(users) > 1: raise AuthException( - strategy.backend, + backend, 'The given email address is associated with another account' ) else: return {'user': users[0]} -def load_extra_data(strategy, details, response, uid, user, *args, **kwargs): - social = kwargs.get('social') or strategy.storage.user.get_social_auth( - strategy.backend.name, - uid - ) +def load_extra_data(backend, details, response, uid, user, *args, **kwargs): + social = kwargs.get('social') or \ + backend.strategy.storage.user.get_social_auth(backend.name, uid) if social: - extra_data = strategy.backend.extra_data(user, uid, response, details) + extra_data = backend.extra_data(user, uid, response, details) social.set_extra_data(extra_data) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 839b665e9..a0c1d0767 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -1,8 +1,10 @@ """SQLAlchemy models for Social Auth""" import base64 import six +import json from sqlalchemy.exc import IntegrityError +from sqlalchemy.types import PickleType, Text from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage @@ -170,3 +172,12 @@ class BaseSQLAlchemyStorage(BaseStorage): @classmethod def is_integrity_error(cls, exception): return exception.__class__ is IntegrityError + + +# JSON type field +class JSONType(PickleType): + impl = Text + + def __init__(self, *args, **kwargs): + kwargs['pickler'] = json + super(JSONType, self).__init__(*args, **kwargs) diff --git a/social/strategies/base.py b/social/strategies/base.py index f0dae3347..84eb5f6a2 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -37,19 +37,14 @@ class BaseStrategy(object): SERIALIZABLE_TYPES = (dict, list, tuple, set, bool, type(None)) + \ six.integer_types + six.string_types + \ (six.text_type, six.binary_type,) + DEFAULT_TEMPLATE_STRATEGY = BaseTemplateStrategy - def __init__(self, backend=None, storage=None, request=None, - tpl=BaseTemplateStrategy, backends=None, *args, **kwargs): - self.tpl = tpl(self) - self.request = request + def __init__(self, storage=None, tpl=None): self.storage = storage - self.backends = backends - self.backend = backend(strategy=self, *args, **kwargs) \ - if backend else None + self.tpl = (tpl or self.DEFAULT_TEMPLATE_STRATEGY)(self) def setting(self, name, default=None, backend=None): names = [setting_name(name), name] - backend = backend or getattr(self, 'backend', None) if backend: names.insert(0, setting_name(backend.name, name)) for name in names: @@ -59,32 +54,6 @@ def setting(self, name, default=None, backend=None): pass return default - def start(self): - # Clean any partial pipeline info before starting the process - self.clean_partial_pipeline() - if self.backend.uses_redirect(): - return self.redirect(self.backend.auth_url()) - else: - return self.html(self.backend.auth_html()) - - def complete(self, *args, **kwargs): - return self.backend.auth_complete(*args, **kwargs) - - def continue_pipeline(self, *args, **kwargs): - return self.backend.continue_pipeline(*args, **kwargs) - - def disconnect(self, user, association_id=None, *args, **kwargs): - return self.backend.disconnect( - user=user, association_id=association_id, - *args, **kwargs - ) - - def authenticate(self, *args, **kwargs): - kwargs['strategy'] = self - kwargs['storage'] = self.storage - kwargs['backend'] = self.backend - return self.backend.authenticate(*args, **kwargs) - def create_user(self, *args, **kwargs): return self.storage.user.create_user(*args, **kwargs) @@ -220,6 +189,14 @@ def render_html(self, tpl=None, html=None, context=None): """Render given template or raw html with given context""" return self.tpl.render(tpl, html, context) + def authenticate(self, backend, *args, **kwargs): + """Trigger the authentication mechanism tied to the current + framework""" + kwargs['strategy'] = self + kwargs['storage'] = self.storage + kwargs['backend'] = backend + return backend.authenticate(*args, **kwargs) + # Implement the following methods on strategies sub-classes def redirect(self, url): diff --git a/social/strategies/cherrypy_strategy.py b/social/strategies/cherrypy_strategy.py index 4f7eb3bd2..d0e9e41f2 100644 --- a/social/strategies/cherrypy_strategy.py +++ b/social/strategies/cherrypy_strategy.py @@ -17,9 +17,7 @@ def render_string(self, html, context): class CherryPyStrategy(BaseStrategy): - def __init__(self, *args, **kwargs): - kwargs.setdefault('tpl', CherryPyJinja2TemplateStrategy) - return super(CherryPyStrategy, self).__init__(*args, **kwargs) + DEFAULT_TEMPLATE_STRATEGY = CherryPyJinja2TemplateStrategy def get_setting(self, name): return cherrypy.config[name] @@ -42,11 +40,11 @@ def redirect(self, url): def html(self, content): return content - def authenticate(self, *args, **kwargs): + def authenticate(self, backend, *args, **kwargs): kwargs['strategy'] = self kwargs['storage'] = self.storage - kwargs['backend'] = self.backend - return self.backend.authenticate(*args, **kwargs) + kwargs['backend'] = backend + return backend.authenticate(*args, **kwargs) def session_get(self, name, default=None): return cherrypy.session.get(name, default) diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 4798ba362..6cc4365f4 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -22,13 +22,12 @@ def render_string(self, html, context): class DjangoStrategy(BaseStrategy): - def __init__(self, *args, **kwargs): - kwargs.setdefault('tpl', DjangoTemplateStrategy) - super(DjangoStrategy, self).__init__(*args, **kwargs) - if self.request: - self.session = self.request.session - else: - self.session = {} + DEFAULT_TEMPLATE_STRATEGY = DjangoTemplateStrategy + + def __init__(self, storage, request=None, tpl=None): + self.request = request + self.session = request.session if request else {} + super(DjangoStrategy, self).__init__(storage, tpl) def get_setting(self, name): return getattr(settings, name) @@ -64,10 +63,10 @@ def render_html(self, tpl=None, html=None, context=None): template = loader.get_template_from_string(html) return template.render(RequestContext(self.request, context)) - def authenticate(self, *args, **kwargs): + def authenticate(self, backend, *args, **kwargs): kwargs['strategy'] = self kwargs['storage'] = self.storage - kwargs['backend'] = self.backend + kwargs['backend'] = backend return authenticate(*args, **kwargs) def session_get(self, name, default=None): diff --git a/social/strategies/flask_strategy.py b/social/strategies/flask_strategy.py index c30a56fbb..d8ebfeaba 100644 --- a/social/strategies/flask_strategy.py +++ b/social/strategies/flask_strategy.py @@ -14,9 +14,7 @@ def render_string(self, html, context): class FlaskStrategy(BaseStrategy): - def __init__(self, *args, **kwargs): - kwargs.setdefault('tpl', FlaskTemplateStrategy) - super(FlaskStrategy, self).__init__(*args, **kwargs) + DEFAULT_TEMPLATE_STRATEGY = FlaskTemplateStrategy def get_setting(self, name): return current_app.config[name] diff --git a/social/strategies/pyramid_strategy.py b/social/strategies/pyramid_strategy.py index d8f49de05..57d8c383f 100644 --- a/social/strategies/pyramid_strategy.py +++ b/social/strategies/pyramid_strategy.py @@ -17,6 +17,12 @@ def render_string(self, html, context): class PyramidStrategy(BaseStrategy): + DEFAULT_TEMPLATE_STRATEGY = PyramidTemplateStrategy + + def __init__(self, storage, request, tpl=None): + self.request = request + super(PyramidStrategy, self).__init__(storage, tpl) + def redirect(self, url): """Return a response redirect to the given URL""" return HTTPFound(location=url) diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index 18349b174..b30b2d1cb 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -16,10 +16,12 @@ def render_string(self, html, context): class TornadoStrategy(BaseStrategy): - def __init__(self, *args, **kwargs): - kwargs.setdefault('tpl', TornadoTemplateStrategy) - self.request_handler = kwargs.get('request_handler') - super(TornadoStrategy, self).__init__(*args, **kwargs) + DEFAULT_TEMPLATE_STRATEGY = TornadoTemplateStrategy + + def __init__(self, storage, request_handler, tpl=None): + self.request_handler = request_handler + self.request = self.request_handler.request + super(TornadoStrategy, self).__init__(storage, tpl) def get_setting(self, name): return self.request_handler.settings[name] diff --git a/social/strategies/utils.py b/social/strategies/utils.py index cc206b18b..6374c6bf5 100644 --- a/social/strategies/utils.py +++ b/social/strategies/utils.py @@ -1,6 +1,4 @@ from social.utils import module_member -from social.exceptions import MissingBackend -from social.backends.utils import get_backend # Current strategy getter cache, currently only used by Django to set a method @@ -11,18 +9,10 @@ _current_strategy_getter = None -def get_strategy(backends, strategy, storage, request=None, backend=None, - *args, **kwargs): - if backend: - Backend = get_backend(backends, backend) - if not Backend: - raise MissingBackend(backend) - else: - Backend = None +def get_strategy(strategy, storage, *args, **kwargs): Strategy = module_member(strategy) Storage = module_member(storage) - return Strategy(Backend, Storage, request, backends=backends, - *args, **kwargs) + return Strategy(Storage, *args, **kwargs) def set_current_strategy_getter(func): diff --git a/social/strategies/webpy_strategy.py b/social/strategies/webpy_strategy.py index 7a8f8008a..5b9a82b47 100644 --- a/social/strategies/webpy_strategy.py +++ b/social/strategies/webpy_strategy.py @@ -12,10 +12,7 @@ def render_string(self, html, context): class WebpyStrategy(BaseStrategy): - def __init__(self, *args, **kwargs): - self.session = web.web_session - kwargs.setdefault('tpl', WebpyTemplateStrategy) - super(WebpyStrategy, self).__init__(*args, **kwargs) + DEFAULT_TEMPLATE_STRATEGY = WebpyTemplateStrategy def get_setting(self, name): return getattr(web.config, name) @@ -50,16 +47,16 @@ def render_html(self, tpl=None, html=None, context=None): return tpl(**context) def session_get(self, name, default=None): - return self.session.get(name, default) + return web.web_session.get(name, default) def session_set(self, name, value): - self.session[name] = value + web.web_session[name] = value def session_pop(self, name): - return self.session.pop(name, None) + return web.web_session.pop(name, None) def session_setdefault(self, name, value): - return self.session.setdefault(name, value) + return web.web_session.setdefault(name, value) def build_absolute_uri(self, path=None): path = path or '' diff --git a/social/tests/actions/actions.py b/social/tests/actions/actions.py index 8e1c15604..243cb1718 100644 --- a/social/tests/actions/actions.py +++ b/social/tests/actions/actions.py @@ -61,8 +61,9 @@ def setUp(self): TestUserSocialAuth.reset_cache() TestNonce.reset_cache() TestAssociation.reset_cache() - self.backend = module_member('social.backends.github.GithubOAuth2') - self.strategy = TestStrategy(self.backend, TestStorage) + Backend = module_member('social.backends.github.GithubOAuth2') + self.strategy = TestStrategy(TestStorage) + self.backend = Backend(self.strategy, redirect_uri='/complete/github') self.user = None def tearDown(self): @@ -86,7 +87,7 @@ def do_login(self, after_complete_checks=True, user_data_body=None, 'social.backends.github.GithubOAuth2', ) }) - start_url = do_auth(self.strategy).url + start_url = do_auth(self.backend).url target_url = self.strategy.build_absolute_uri( '/complete/github/?code=foobar' ) @@ -118,10 +119,10 @@ def do_login(self, after_complete_checks=True, user_data_body=None, content_type='text/json') self.strategy.set_request_data(location_query) redirect = do_complete( - self.strategy, + self.backend, user=self.user, - login=lambda strategy, user, social_user: - strategy.session_set('username', user.username) + login=lambda backend, user, social_user: + backend.strategy.session_set('username', user.username) ) if after_complete_checks: expect(self.strategy.session_get('username')).to.equal( @@ -153,7 +154,7 @@ def do_login_with_partial_pipeline(self, before_complete=None): 'social.pipeline.user.user_details' ) }) - start_url = do_auth(self.strategy).url + start_url = do_auth(self.backend).url target_url = self.strategy.build_absolute_uri( '/complete/github/?code=foobar' ) @@ -184,10 +185,10 @@ def do_login_with_partial_pipeline(self, before_complete=None): content_type='text/json') self.strategy.set_request_data(location_query) - def _login(strategy, user, social_user): - strategy.session_set('username', user.username) + def _login(backend, user, social_user): + backend.strategy.session_set('username', user.username) - redirect = do_complete(self.strategy, user=self.user, login=_login) + redirect = do_complete(self.backend, user=self.user, login=_login) url = self.strategy.build_absolute_uri('/password') expect(redirect.url).to.equal(url) HTTPretty.register_uri(HTTPretty.GET, redirect.url, status=200, @@ -203,7 +204,7 @@ def _login(strategy, user, social_user): if before_complete: before_complete() - redirect = do_complete(self.strategy, user=self.user, login=_login) + redirect = do_complete(self.backend, user=self.user, login=_login) expect(self.strategy.session_get('username')).to.equal( self.expected_username ) diff --git a/social/tests/actions/test_disconnect.py b/social/tests/actions/test_disconnect.py index 637287c67..78c0f2ddb 100644 --- a/social/tests/actions/test_disconnect.py +++ b/social/tests/actions/test_disconnect.py @@ -15,7 +15,7 @@ class DisconnectActionTest(BaseActionTest): def test_not_allowed_to_disconnect(self): self.do_login() user = User.get(self.expected_username) - do_disconnect.when.called_with(self.strategy, user).should.throw( + do_disconnect.when.called_with(self.backend, user).should.throw( NotAllowedToDisconnect ) @@ -23,7 +23,7 @@ def test_disconnect(self): self.do_login() user = User.get(self.expected_username) user.password = 'password' - do_disconnect(self.strategy, user) + do_disconnect(self.backend, user) expect(len(user.social)).to.equal(0) def test_disconnect_with_partial_pipeline(self): @@ -40,7 +40,7 @@ def test_disconnect_with_partial_pipeline(self): }) self.do_login() user = User.get(self.expected_username) - redirect = do_disconnect(self.strategy, user) + redirect = do_disconnect(self.backend, user) url = self.strategy.build_absolute_uri('/password') expect(redirect.url).to.equal(url) @@ -55,5 +55,5 @@ def test_disconnect_with_partial_pipeline(self): expect(data['password']).to.equal(password) self.strategy.session_set('password', data['password']) - redirect = do_disconnect(self.strategy, user) + redirect = do_disconnect(self.backend, user) expect(len(user.social)).to.equal(0) diff --git a/social/tests/backends/base.py b/social/tests/backends/base.py index b791b0162..12a22c769 100644 --- a/social/tests/backends/base.py +++ b/social/tests/backends/base.py @@ -20,8 +20,9 @@ class BaseBackendTest(unittest.TestCase): def setUp(self): HTTPretty.enable() - self.backend = module_member(self.backend_path) - self.strategy = TestStrategy(self.backend, TestStorage) + Backend = module_member(self.backend_path) + self.strategy = TestStrategy(TestStorage) + self.backend = Backend(self.strategy, redirect_uri=self.complete_url) self.name = self.backend.name.upper().replace('-', '_') self.complete_url = self.strategy.build_absolute_uri( self.raw_complete_url.format(self.backend.name) diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index 0282653b2..f4e716569 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -53,11 +53,11 @@ class OpenIdTest(BaseBackendTest): def setUp(self): HTTPretty.enable() - self.backend = module_member(self.backend_path) + Backend = module_member(self.backend_path) name = self.backend.name self.complete_url = self.raw_complete_url.format(name) - self.strategy = TestStrategy(self.backend, TestStorage, - redirect_uri=self.complete_url) + self.strategy = TestStrategy(TestStorage) + self.backend = Backend(self.strategy, redirect_uri=self.complete_url) self.strategy.set_settings({ 'SOCIAL_AUTH_AUTHENTICATION_BACKENDS': ( self.backend_path, diff --git a/social/tests/backends/test_dummy.py b/social/tests/backends/test_dummy.py index bb8f0bd4e..b7bb1e445 100644 --- a/social/tests/backends/test_dummy.py +++ b/social/tests/backends/test_dummy.py @@ -74,11 +74,10 @@ def test_revoke_token(self): self.do_login() user = User.get(self.expected_username) user.password = 'password' - backend = self.backend - HTTPretty.register_uri(self._method(backend.REVOKE_TOKEN_METHOD), - backend.REVOKE_TOKEN_URL, + HTTPretty.register_uri(self._method(self.backend.REVOKE_TOKEN_METHOD), + self.backend.REVOKE_TOKEN_URL, status=200) - do_disconnect(self.strategy, user) + do_disconnect(self.backend, user) class WhitelistEmailsTest(DummyOAuth2Test): diff --git a/social/tests/backends/test_google.py b/social/tests/backends/test_google.py index bef2d088b..4f1a40c66 100644 --- a/social/tests/backends/test_google.py +++ b/social/tests/backends/test_google.py @@ -276,8 +276,7 @@ def test_revoke_token(self): self.do_login() user = User.get(self.expected_username) user.password = 'password' - backend = self.backend - HTTPretty.register_uri(self._method(backend.REVOKE_TOKEN_METHOD), - backend.REVOKE_TOKEN_URL, + HTTPretty.register_uri(self._method(self.backend.REVOKE_TOKEN_METHOD), + self.backend.REVOKE_TOKEN_URL, status=200) - do_disconnect(self.strategy, user) + do_disconnect(self.backend, user) diff --git a/social/tests/strategy.py b/social/tests/strategy.py index eb135678d..a70a2fe44 100644 --- a/social/tests/strategy.py +++ b/social/tests/strategy.py @@ -19,12 +19,13 @@ def render_string(self, html, context): class TestStrategy(BaseStrategy): - def __init__(self, *args, **kwargs): + DEFAULT_TEMPLATE_STRATEGY = TestTemplateStrategy + + def __init__(self, storage, tpl=None): self._request_data = {} self._settings = {} self._session = {} - kwargs.setdefault('tpl', TestTemplateStrategy) - super(TestStrategy, self).__init__(*args, **kwargs) + super(TestStrategy, self).__init__(storage, tpl) def redirect(self, url): return Redirect(url) diff --git a/social/tests/test_pipeline.py b/social/tests/test_pipeline.py index 2c39caa5b..3e8c5dca4 100644 --- a/social/tests/test_pipeline.py +++ b/social/tests/test_pipeline.py @@ -58,7 +58,7 @@ class UnknownErrorStorage(IntegrityErrorStorage): class IntegrityErrorOnLoginTest(BaseActionTest): def setUp(self): super(IntegrityErrorOnLoginTest, self).setUp() - self.strategy = TestStrategy(self.backend, IntegrityErrorStorage) + self.strategy = TestStrategy(IntegrityErrorStorage) def test_integrity_error(self): self.do_login() @@ -67,7 +67,7 @@ def test_integrity_error(self): class UnknownErrorOnLoginTest(BaseActionTest): def setUp(self): super(UnknownErrorOnLoginTest, self).setUp() - self.strategy = TestStrategy(self.backend, UnknownErrorStorage) + self.strategy = TestStrategy(UnknownErrorStorage) def test_unknown_error(self): self.do_login.when.called_with().should.throw(UnknownError) diff --git a/social/utils.py b/social/utils.py index a87d024d3..c1053349a 100644 --- a/social/utils.py +++ b/social/utils.py @@ -121,19 +121,20 @@ def drop_lists(value): return out -def partial_pipeline_data(strategy, user=None, *args, **kwargs): - partial = strategy.session_get('partial_pipeline', None) +def partial_pipeline_data(backend, user=None, *args, **kwargs): + partial = backend.strategy.session_get('partial_pipeline', None) if partial: - idx, backend, xargs, xkwargs = strategy.partial_from_session(partial) - if backend == strategy.backend.name: + idx, backend_name, xargs, xkwargs = \ + backend.strategy.partial_from_session(partial) + if backend_name == backend.name: kwargs.setdefault('pipeline_index', idx) if user: # don't update user if it's None kwargs.setdefault('user', user) - kwargs.setdefault('request', strategy.request) + kwargs.setdefault('request', backend.strategy.request_data()) xkwargs.update(kwargs) return xargs, xkwargs else: - strategy.clean_partial_pipeline() + backend.strategy.clean_partial_pipeline() def build_absolute_uri(host_url, path=None): @@ -171,11 +172,11 @@ def is_url(value): value.startswith('/')) -def setting_url(strategy, *names): +def setting_url(backend, *names): for name in names: if is_url(name): return name else: - value = strategy.setting(name) + value = backend.setting(name) if is_url(value): return value From 37e3b8f561d0fd444dbf94e8d1aba27272121a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Jun 2014 17:52:30 -0300 Subject: [PATCH 257/890] Version change (no backward compatible change) --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index e40784383..27782b198 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 1, 26) -extra = '' +version = (0, 2, 0) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 23e4e289ec426732324af106c7c2e24efea34aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 8 Jun 2014 05:08:57 -0300 Subject: [PATCH 258/890] Update tests --- social/actions.py | 9 ++++----- social/backends/base.py | 4 ++-- social/pipeline/social_auth.py | 2 +- social/storage/base.py | 2 +- social/strategies/base.py | 4 ++++ social/tests/actions/actions.py | 22 +++++++++++++--------- social/tests/actions/test_associate.py | 1 + social/tests/actions/test_login.py | 4 ++-- social/tests/backends/base.py | 10 +++++----- social/tests/backends/legacy.py | 6 +++--- social/tests/backends/oauth.py | 7 ++++--- social/tests/backends/open_id.py | 12 ++++++------ social/tests/backends/test_livejournal.py | 6 ++++-- social/tests/backends/test_utils.py | 8 +++++--- social/tests/strategy.py | 3 ++- social/tests/test_pipeline.py | 4 ++-- social/tests/test_utils.py | 22 +++++++++++----------- 17 files changed, 70 insertions(+), 56 deletions(-) diff --git a/social/actions.py b/social/actions.py index 1781cb216..0001f0351 100644 --- a/social/actions.py +++ b/social/actions.py @@ -90,16 +90,15 @@ def do_complete(backend, login, user=None, redirect_name='next', def do_disconnect(backend, user, association_id=None, redirect_name='next', *args, **kwargs): - partial = partial_pipeline_data(backend.strategy, user, *args, **kwargs) + partial = partial_pipeline_data(backend, user, *args, **kwargs) if partial: xargs, xkwargs = partial if association_id and not xkwargs.get('association_id'): xkwargs['association_id'] = association_id - response = backend.strategy.disconnect(*xargs, **xkwargs) + response = backend.disconnect(*xargs, **xkwargs) else: - response = backend.strategy.disconnect(user=user, - association_id=association_id, - *args, **kwargs) + response = backend.disconnect(user=user, association_id=association_id, + *args, **kwargs) if isinstance(response, dict): response = backend.strategy.redirect( diff --git a/social/backends/base.py b/social/backends/base.py index d334970f9..41da87c5f 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -93,7 +93,7 @@ def disconnect(self, *args, **kwargs): pipeline = self.strategy.get_disconnect_pipeline() if 'pipeline_index' in kwargs: pipeline = pipeline[kwargs['pipeline_index']:] - kwargs['name'] = self.strategy.backend.name + kwargs['name'] = self.name kwargs['user_storage'] = self.strategy.storage.user return self.run_pipeline(pipeline, *args, **kwargs) @@ -186,7 +186,7 @@ def get_user(self, user_id): def continue_pipeline(self, *args, **kwargs): """Continue previous halted pipeline""" - kwargs.update({'backend': self}) + kwargs.update({'backend': self, 'strategy': self.strategy}) return self.authenticate(*args, **kwargs) def request_token_extra_arguments(self): diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 419153d7a..90aae6438 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -42,7 +42,7 @@ def associate_user(backend, uid, user=None, social=None, *args, **kwargs): # Protect for possible race condition, those bastard with FTL # clicking capabilities, check issue #131: # https://github.com/omab/django-social-auth/issues/131 - return social_user(backend.strategy, uid, user, *args, **kwargs) + return social_user(backend, uid, user, *args, **kwargs) else: return {'social': social, 'user': social.user, diff --git a/social/storage/base.py b/social/storage/base.py index b8d4900b0..71afc0096 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -25,7 +25,7 @@ class UserMixin(object): def get_backend(self, strategy=None): strategy = strategy or get_current_strategy() if strategy: - return get_backend(strategy.backends, self.provider) + return get_backend(strategy.get_backends(), self.provider) def get_backend_instance(self, strategy=None): strategy = strategy or get_current_strategy() diff --git a/social/strategies/base.py b/social/strategies/base.py index 84eb5f6a2..2e9f2dd1f 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -197,6 +197,10 @@ def authenticate(self, backend, *args, **kwargs): kwargs['backend'] = backend return backend.authenticate(*args, **kwargs) + def get_backends(self): + """Return configured backends""" + return self.setting('AUTHENTICATION_BACKENDS', []) + # Implement the following methods on strategies sub-classes def redirect(self, url): diff --git a/social/tests/actions/actions.py b/social/tests/actions/actions.py index 243cb1718..14a8f6495 100644 --- a/social/tests/actions/actions.py +++ b/social/tests/actions/actions.py @@ -55,6 +55,10 @@ class BaseActionTest(unittest.TestCase): } }) + def __init__(self, *args, **kwargs): + self.strategy = None + super(BaseActionTest, self).__init__(*args, **kwargs) + def setUp(self): HTTPretty.enable() User.reset_cache() @@ -62,7 +66,7 @@ def setUp(self): TestNonce.reset_cache() TestAssociation.reset_cache() Backend = module_member('social.backends.github.GithubOAuth2') - self.strategy = TestStrategy(TestStorage) + self.strategy = self.strategy or TestStrategy(TestStorage) self.backend = Backend(self.strategy, redirect_uri='/complete/github') self.user = None @@ -117,13 +121,13 @@ def do_login(self, after_complete_checks=True, user_data_body=None, HTTPretty.register_uri(HTTPretty.GET, self.user_data_url, body=user_data_body, content_type='text/json') - self.strategy.set_request_data(location_query) - redirect = do_complete( - self.backend, - user=self.user, - login=lambda backend, user, social_user: - backend.strategy.session_set('username', user.username) - ) + self.strategy.set_request_data(location_query, self.backend) + + def _login(backend, user, social_user): + backend.strategy.session_set('username', user.username) + + redirect = do_complete(self.backend, user=self.user, login=_login) + if after_complete_checks: expect(self.strategy.session_get('username')).to.equal( expected_username or self.expected_username @@ -183,7 +187,7 @@ def do_login_with_partial_pipeline(self, before_complete=None): HTTPretty.register_uri(HTTPretty.GET, self.user_data_url, body=self.user_data_body or '', content_type='text/json') - self.strategy.set_request_data(location_query) + self.strategy.set_request_data(location_query, self.backend) def _login(backend, user, social_user): backend.strategy.session_set('username', user.username) diff --git a/social/tests/actions/test_associate.py b/social/tests/actions/test_associate.py index 087582924..1f2c342c1 100644 --- a/social/tests/actions/test_associate.py +++ b/social/tests/actions/test_associate.py @@ -13,6 +13,7 @@ class AssociateActionTest(BaseActionTest): def setUp(self): super(AssociateActionTest, self).setUp() self.user = User(username='foobar', email='foo@bar.com') + self.backend.strategy.session_set('username', self.user.username) def test_associate(self): self.do_login() diff --git a/social/tests/actions/test_login.py b/social/tests/actions/test_login.py index 98240ea5e..e2339d4cc 100644 --- a/social/tests/actions/test_login.py +++ b/social/tests/actions/test_login.py @@ -15,13 +15,13 @@ def test_fields_stored_in_session(self): self.strategy.set_settings({ 'SOCIAL_AUTH_FIELDS_STORED_IN_SESSION': ['foo', 'bar'] }) - self.strategy.set_request_data({'foo': '1', 'bar': '2'}) + self.strategy.set_request_data({'foo': '1', 'bar': '2'}, self.backend) self.do_login() expect(self.strategy.session_get('foo')).to.equal('1') expect(self.strategy.session_get('bar')).to.equal('2') def test_redirect_value(self): - self.strategy.set_request_data({'next': '/after-login'}) + self.strategy.set_request_data({'next': '/after-login'}, self.backend) redirect = self.do_login(after_complete_checks=False) expect(redirect.url).to.equal('/after-login') diff --git a/social/tests/backends/base.py b/social/tests/backends/base.py index 12a22c769..15bb98fd3 100644 --- a/social/tests/backends/base.py +++ b/social/tests/backends/base.py @@ -65,7 +65,7 @@ def do_login(self): expect(user.username).to.equal(username) expect(self.strategy.session_get('username')).to.equal(username) expect(self.strategy.get_user(user.id)).to.equal(user) - expect(self.strategy.backend.get_user(user.id)).to.equal(user) + expect(self.backend.get_user(user.id)).to.equal(user) user_backends = user_backends_data( user, self.strategy.get_setting('SOCIAL_AUTH_AUTHENTICATION_BACKENDS'), @@ -136,8 +136,8 @@ def do_partial_pipeline(self): data = self.strategy.session_pop('partial_pipeline') idx, backend, xargs, xkwargs = self.strategy.partial_from_session(data) expect(backend).to.equal(self.backend.name) - redirect = self.strategy.continue_pipeline(pipeline_index=idx, - *xargs, **xkwargs) + redirect = self.backend.continue_pipeline(pipeline_index=idx, + *xargs, **xkwargs) url = self.strategy.build_absolute_uri('/slug') expect(redirect.url).to.equal(url) @@ -147,8 +147,8 @@ def do_partial_pipeline(self): data = self.strategy.session_pop('partial_pipeline') idx, backend, xargs, xkwargs = self.strategy.partial_from_session(data) expect(backend).to.equal(self.backend.name) - user = self.strategy.continue_pipeline(pipeline_index=idx, - *xargs, **xkwargs) + user = self.backend.continue_pipeline(pipeline_index=idx, + *xargs, **xkwargs) expect(user.username).to.equal(self.expected_username) expect(user.slug).to.equal(slug) diff --git a/social/tests/backends/legacy.py b/social/tests/backends/legacy.py index f652c262b..4204fab11 100644 --- a/social/tests/backends/legacy.py +++ b/social/tests/backends/legacy.py @@ -25,7 +25,7 @@ def extra_settings(self): '/login/{0}'.format(self.backend.name)} def do_start(self): - start_url = self.strategy.build_absolute_uri(self.strategy.start().url) + start_url = self.strategy.build_absolute_uri(self.backend.start().url) HTTPretty.register_uri( HTTPretty.GET, start_url, @@ -45,5 +45,5 @@ def do_start(self): self.complete_url, data=parse_qs(self.response_body) ) - self.strategy.set_request_data(parse_qs(response.text)) - return self.strategy.complete() + self.strategy.set_request_data(parse_qs(response.text), self.backend) + return self.backend.complete() diff --git a/social/tests/backends/oauth.py b/social/tests/backends/oauth.py index 12364cbad..87961ac46 100644 --- a/social/tests/backends/oauth.py +++ b/social/tests/backends/oauth.py @@ -66,13 +66,14 @@ def auth_handlers(self, start_url): return target_url def do_start(self): - start_url = self.strategy.start().url + start_url = self.backend.start().url target_url = self.auth_handlers(start_url) response = requests.get(start_url) expect(response.url).to.equal(target_url) expect(response.text).to.equal('foobar') - self.strategy.set_request_data(parse_qs(urlparse(target_url).query)) - return self.strategy.complete() + self.strategy.set_request_data(parse_qs(urlparse(target_url).query), + self.backend) + return self.backend.complete() class OAuth1Test(BaseOAuthTest): diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index f4e716569..1af517d77 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -54,9 +54,8 @@ class OpenIdTest(BaseBackendTest): def setUp(self): HTTPretty.enable() Backend = module_member(self.backend_path) - name = self.backend.name - self.complete_url = self.raw_complete_url.format(name) self.strategy = TestStrategy(TestStorage) + self.complete_url = self.raw_complete_url.format(Backend.name) self.backend = Backend(self.strategy, redirect_uri=self.complete_url) self.strategy.set_settings({ 'SOCIAL_AUTH_AUTHENTICATION_BACKENDS': ( @@ -84,7 +83,7 @@ def get_form_data(self, html): return parser.form, parser.inputs def openid_url(self): - return self.strategy.backend.openid_url() + return self.backend.openid_url() def post_start(self): pass @@ -95,7 +94,7 @@ def do_start(self): status=200, body=self.discovery_body, content_type='application/xrds+xml') - start = self.strategy.start() + start = self.backend.start() self.post_start() form, inputs = self.get_form_data(start) HTTPretty.register_uri(HTTPretty.POST, @@ -103,9 +102,10 @@ def do_start(self): status=200, body=self.server_response) response = requests.post(form.get('action'), data=inputs) - self.strategy.set_request_data(parse_qs(response.content)) + self.strategy.set_request_data(parse_qs(response.content), + self.backend) HTTPretty.register_uri(HTTPretty.POST, form.get('action'), status=200, body='is_valid:true\n') - return self.strategy.complete() + return self.backend.complete() diff --git a/social/tests/backends/test_livejournal.py b/social/tests/backends/test_livejournal.py index 258f85b0f..041ce3f04 100644 --- a/social/tests/backends/test_livejournal.py +++ b/social/tests/backends/test_livejournal.py @@ -83,12 +83,14 @@ def _setup_handlers(self): ) def test_login(self): - self.strategy.set_request_data({'openid_lj_user': 'foobar'}) + self.strategy.set_request_data({'openid_lj_user': 'foobar'}, + self.backend) self._setup_handlers() self.do_login() def test_partial_pipeline(self): - self.strategy.set_request_data({'openid_lj_user': 'foobar'}) + self.strategy.set_request_data({'openid_lj_user': 'foobar'}, + self.backend) self._setup_handlers() self.do_partial_pipeline() diff --git a/social/tests/backends/test_utils.py b/social/tests/backends/test_utils.py index a1e07a264..9ba25d2b1 100644 --- a/social/tests/backends/test_utils.py +++ b/social/tests/backends/test_utils.py @@ -5,6 +5,7 @@ from social.tests.strategy import TestStrategy from social.backends.utils import load_backends, get_backend from social.backends.github import GithubOAuth2 +from social.exceptions import MissingBackend class BaseBackendUtilsTest(unittest.TestCase): @@ -41,9 +42,10 @@ def test_get_backend(self): expect(backend).to.equal(GithubOAuth2) def test_get_missing_backend(self): - backend = get_backend(( + get_backend.when.called_with(( 'social.backends.github.GithubOAuth2', 'social.backends.facebook.FacebookOAuth2', 'social.backends.flickr.FlickrOAuth' - ), 'foobar') - expect(backend).to.equal(None) + ), 'foobar').should.throw( + MissingBackend, 'Missing backend "foobar" entry' + ) diff --git a/social/tests/strategy.py b/social/tests/strategy.py index a70a2fe44..9ccd7d04f 100644 --- a/social/tests/strategy.py +++ b/social/tests/strategy.py @@ -72,8 +72,9 @@ def build_absolute_uri(self, path=None): def set_settings(self, values): self._settings.update(values) - def set_request_data(self, values): + def set_request_data(self, values, backend): self._request_data.update(values) + backend.data = self._request_data def remove_from_request_data(self, name): self._request_data.pop(name, None) diff --git a/social/tests/test_pipeline.py b/social/tests/test_pipeline.py index 3e8c5dca4..fd7778320 100644 --- a/social/tests/test_pipeline.py +++ b/social/tests/test_pipeline.py @@ -57,8 +57,8 @@ class UnknownErrorStorage(IntegrityErrorStorage): class IntegrityErrorOnLoginTest(BaseActionTest): def setUp(self): - super(IntegrityErrorOnLoginTest, self).setUp() self.strategy = TestStrategy(IntegrityErrorStorage) + super(IntegrityErrorOnLoginTest, self).setUp() def test_integrity_error(self): self.do_login() @@ -66,8 +66,8 @@ def test_integrity_error(self): class UnknownErrorOnLoginTest(BaseActionTest): def setUp(self): - super(UnknownErrorOnLoginTest, self).setUp() self.strategy = TestStrategy(UnknownErrorStorage) + super(UnknownErrorOnLoginTest, self).setUp() def test_unknown_error(self): self.do_login.when.called_with().should.throw(UnknownError) diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index bb5913956..bde7a8463 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -124,26 +124,26 @@ def test_absolute_uri(self): class PartialPipelineData(unittest.TestCase): def test_kwargs_included_in_result(self): - strategy = self._strategy() + backend = self._backend() kwargitem = ('foo', 'bar') - _, xkwargs = partial_pipeline_data(strategy, None, + _, xkwargs = partial_pipeline_data(backend, None, *(), **dict([kwargitem])) xkwargs.should.have.key(kwargitem[0]).being.equal(kwargitem[1]) def test_update_user(self): user = object() - strategy = self._strategy(session_kwargs={'user': None}) - _, xkwargs = partial_pipeline_data(strategy, user) + backend = self._backend(session_kwargs={'user': None}) + _, xkwargs = partial_pipeline_data(backend, user) xkwargs.should.have.key('user').being.equal(user) - def _strategy(self, session_kwargs=None): - backend = Mock() - backend.name = 'mock-backend' - + def _backend(self, session_kwargs=None): strategy = Mock() strategy.request = None - strategy.backend = backend strategy.session_get.return_value = object() strategy.partial_from_session.return_value = \ - (0, backend.name, [], session_kwargs or {}) - return strategy + (0, 'mock-backend', [], session_kwargs or {}) + + backend = Mock() + backend.name = 'mock-backend' + backend.strategy = strategy + return backend From 97f9c83e9074e95e7fcb9efe0dab31656992a227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 8 Jun 2014 17:04:45 -0300 Subject: [PATCH 259/890] Set user backend reference in django app --- social/apps/django_app/views.py | 2 ++ social/strategies/base.py | 52 +++------------------------------ 2 files changed, 6 insertions(+), 48 deletions(-) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 34039f8d8..a20be1ba1 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -32,6 +32,8 @@ def disconnect(request, backend, association_id=None): def _do_login(backend, user, social_user): + user.backend = '{}.{}'.format(backend.__module__, + backend.__class__.__name__) login(backend.strategy.request, user) if backend.setting('SESSION_EXPIRATION', True): # Set session expiration date if present and not disabled diff --git a/social/strategies/base.py b/social/strategies/base.py index 2e9f2dd1f..5c40eb6d4 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -7,6 +7,7 @@ from social.utils import setting_name, module_member from social.store import OpenIdStore, OpenIdSessionWrapper from social.pipeline import DEFAULT_AUTH_PIPELINE, DEFAULT_DISCONNECT_PIPELINE +from social.pipeline.utils import partial_from_session, partial_to_session class BaseTemplateStrategy(object): @@ -87,56 +88,11 @@ def from_session_value(self, val): return val def partial_to_session(self, next, backend, request=None, *args, **kwargs): - user = kwargs.get('user') - social = kwargs.get('social') - clean_kwargs = { - 'response': kwargs.get('response') or {}, - 'details': kwargs.get('details') or {}, - 'username': kwargs.get('username'), - 'uid': kwargs.get('uid'), - 'is_new': kwargs.get('is_new') or False, - 'new_association': kwargs.get('new_association') or False, - 'user': user and user.id or None, - 'social': social and { - 'provider': social.provider, - 'uid': social.uid - } or None - } - clean_kwargs.update(kwargs) - - # Clean any MergeDict data type from the values - kwargs = {} - for name, value in clean_kwargs.items(): - # Check for class name to avoid importing Django MergeDict or - # Werkzeug MultiDict - if isinstance(value, dict) or \ - value.__class__.__name__ in ('MergeDict', 'MultiDict'): - value = dict(value) - if isinstance(value, self.SERIALIZABLE_TYPES): - kwargs[name] = self.to_session_value(value) - - return { - 'next': next, - 'backend': backend.name, - 'args': tuple(map(self.to_session_value, args)), - 'kwargs': kwargs - } + return partial_to_session(self, next, backend, request=request, + *args, **kwargs) def partial_from_session(self, session): - kwargs = session['kwargs'].copy() - user = kwargs.get('user') - social = kwargs.get('social') - if isinstance(social, dict): - kwargs['social'] = self.storage.user.get_social_auth(**social) - if user: - kwargs['user'] = self.storage.user.get_user(user) - return ( - session['next'], - session['backend'], - list(map(self.from_session_value, session['args'])), - dict((key, self.from_session_value(val)) - for key, val in kwargs.items()) - ) + return partial_from_session(self, session) def clean_partial_pipeline(self, name='partial_pipeline'): self.session_pop(name) From d5840114191cffc4670a78ac538b402978185ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 8 Jun 2014 17:05:21 -0300 Subject: [PATCH 260/890] Add missing module --- social/pipeline/utils.py | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 social/pipeline/utils.py diff --git a/social/pipeline/utils.py b/social/pipeline/utils.py new file mode 100644 index 000000000..eae1c3a1c --- /dev/null +++ b/social/pipeline/utils.py @@ -0,0 +1,60 @@ +import six + + +SERIALIZABLE_TYPES = (dict, list, tuple, set, bool, type(None)) + \ + six.integer_types + six.string_types + \ + (six.text_type, six.binary_type,) + + +def partial_to_session(strategy, next, backend, request=None, *args, **kwargs): + user = kwargs.get('user') + social = kwargs.get('social') + clean_kwargs = { + 'response': kwargs.get('response') or {}, + 'details': kwargs.get('details') or {}, + 'username': kwargs.get('username'), + 'uid': kwargs.get('uid'), + 'is_new': kwargs.get('is_new') or False, + 'new_association': kwargs.get('new_association') or False, + 'user': user and user.id or None, + 'social': social and { + 'provider': social.provider, + 'uid': social.uid + } or None + } + clean_kwargs.update(kwargs) + + # Clean any MergeDict data type from the values + kwargs = {} + for name, value in clean_kwargs.items(): + # Check for class name to avoid importing Django MergeDict or + # Werkzeug MultiDict + if isinstance(value, dict) or \ + value.__class__.__name__ in ('MergeDict', 'MultiDict'): + value = dict(value) + if isinstance(value, SERIALIZABLE_TYPES): + kwargs[name] = strategy.to_session_value(value) + + return { + 'next': next, + 'backend': backend.name, + 'args': tuple(map(strategy.to_session_value, args)), + 'kwargs': kwargs + } + + +def partial_from_session(strategy, session): + kwargs = session['kwargs'].copy() + user = kwargs.get('user') + social = kwargs.get('social') + if isinstance(social, dict): + kwargs['social'] = strategy.storage.user.get_social_auth(**social) + if user: + kwargs['user'] = strategy.storage.user.get_user(user) + return ( + session['next'], + session['backend'], + list(map(strategy.from_session_value, session['args'])), + dict((key, strategy.from_session_value(val)) + for key, val in kwargs.items()) + ) From bbebc49335cb988c9ca66ef277921e404ed60be4 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Mon, 9 Jun 2014 10:46:41 -0700 Subject: [PATCH 261/890] Update docker backend with Docker Hub endpoints Changes the backend domains from www.docker.io to hub.docker.com --- social/backends/docker.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/social/backends/docker.py b/social/backends/docker.py index ea73dbd6a..92513ca24 100644 --- a/social/backends/docker.py +++ b/social/backends/docker.py @@ -1,5 +1,5 @@ """ -Docker.io OAuth2 backend, docs at: +Docker Hub OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/docker.html """ from social.backends.oauth import BaseOAuth2 @@ -8,9 +8,9 @@ class DockerOAuth2(BaseOAuth2): name = 'docker' ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://www.docker.io/api/v1.1/o/authorize/' - ACCESS_TOKEN_URL = 'https://www.docker.io/api/v1.1/o/token/' - REFRESH_TOKEN_URL = 'https://www.docker.io/api/v1.1/o/token/' + AUTHORIZATION_URL = 'https://hub.docker.com/api/v1.1/o/authorize/' + ACCESS_TOKEN_URL = 'https://hub.docker.com/api/v1.1/o/token/' + REFRESH_TOKEN_URL = 'https://hub.docker.com/api/v1.1/o/token/' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False EXTRA_DATA = [ @@ -25,7 +25,7 @@ class DockerOAuth2(BaseOAuth2): ] def get_user_details(self, response): - """Return user details from Docker.io account""" + """Return user details from Docker Hub account""" fullname, first_name, last_name = self.get_user_names( response.get('full_name') or response.get('username') or '' ) @@ -38,9 +38,9 @@ def get_user_details(self, response): } def user_data(self, access_token, *args, **kwargs): - """Grab user profile information from Docker.io.""" + """Grab user profile information from Docker Hub.""" username = kwargs['response']['username'] return self.get_json( - 'https://www.docker.io/api/v1.1/users/%s/' % username, + 'https://hub.docker.com/api/v1.1/users/%s/' % username, headers={'Authorization': 'Bearer %s' % access_token} ) From 2a8bedb530330616ccfed79aef39011047869ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 12 Jun 2014 23:50:43 -0300 Subject: [PATCH 262/890] QQ backend --- docs/backends/index.rst | 1 + docs/backends/qq.rst | 27 +++++++++++++++++++++ social/backends/qq.py | 53 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 docs/backends/qq.rst create mode 100644 social/backends/qq.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index b3ccf9476..fdf01ed49 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -88,6 +88,7 @@ Social backends pixelpin pocket podio + qq rdio readability reddit diff --git a/docs/backends/qq.rst b/docs/backends/qq.rst new file mode 100644 index 000000000..2f3a932d3 --- /dev/null +++ b/docs/backends/qq.rst @@ -0,0 +1,27 @@ +QQ +== + +QQ implemented OAuth2 protocol for their authentication mechanism. To enable +``python-social-auth`` support follow this steps: + +1. Go to `QQ`_ and create an application. + +2. Fill App Id and Secret in your project settings:: + + SOCIAL_AUTH_QQ_KEY = '...' + SOCIAL_AUTH_QQ_SECRET = '...' + +3. Enable the backend:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.qq.QQOauth2', + ... + ) + + +The values for ``nickname``, ``figureurl_qq_1`` and ``gender`` will be stored +in the ``extra_data`` field. The ``nickname`` will be used as the account +username. ``figureurl_qq_1`` can be used as the profile image. + +.. _QQ: http://connect.qq.com/ diff --git a/social/backends/qq.py b/social/backends/qq.py new file mode 100644 index 000000000..0a50df77a --- /dev/null +++ b/social/backends/qq.py @@ -0,0 +1,53 @@ +""" +Created on May 13, 2014 + +@author: Yong Zhang (zyfyfe@gmail.com) +""" + +import json + +from social.utils import parse_qs +from social.backends.oauth import BaseOAuth2 + + +class QQOAuth2(BaseOAuth2): + name = 'qq' + ID_KEY = 'openid' + AUTHORIZE_URL = 'https://graph.qq.com/oauth2.0/authorize' + ACCESS_TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' + AUTHORIZATION_URL = 'https://graph.qq.com/oauth2.0/authorize' + OPENID_URL = 'https://graph.qq.com/oauth2.0/me' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('nickname', 'username'), + ('figureurl_qq_1', 'profile_image_url'), + ('gender', 'gender') + ] + + def get_user_details(self, response): + return { + 'username': response.get('nickname', '') + } + + def get_openid(self, access_token): + response = self.request(self.OPENID_URL, params={ + 'access_token': access_token + }) + data = json.loads(response.content[10:-3]) + return data['openid'] + + def user_data(self, access_token, *args, **kwargs): + openid = self.get_openid(access_token) + response = self.get_json( + 'https://graph.qq.com/user/get_user_info', params={ + 'access_token': access_token, + 'oauth_consumer_key': self.setting('SOCIAL_AUTH_QQ_KEY'), + 'openid': openid + } + ) + response['openid'] = openid + return response + + def request_access_token(self, url, data, *args, **kwargs): + response = self.request(url, params=data, *args, **kwargs) + return parse_qs(response.content) From 995733dfad5fddc4f1604a80eb2707ca18ecc586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 14 Jun 2014 00:20:28 -0300 Subject: [PATCH 263/890] Use mongoengin ORM in django me app --- examples/django_me_example/requirements.txt | 2 +- social/apps/django_app/me/models.py | 99 ++++----------------- social/storage/base.py | 9 +- 3 files changed, 25 insertions(+), 85 deletions(-) diff --git a/examples/django_me_example/requirements.txt b/examples/django_me_example/requirements.txt index e93d7642f..37f1c1a53 100644 --- a/examples/django_me_example/requirements.txt +++ b/examples/django_me_example/requirements.txt @@ -1,3 +1,3 @@ django>=1.4 -mongoengine==0.8.4 +mongoengine>=0.8.6 python-social-auth diff --git a/social/apps/django_app/me/models.py b/social/apps/django_app/me/models.py index 33a484c54..94770343b 100644 --- a/social/apps/django_app/me/models.py +++ b/social/apps/django_app/me/models.py @@ -1,22 +1,19 @@ """ -MongoEngine models for Social Auth - -Requires MongoEngine 0.6.10 +MongoEngine Django models for Social Auth. +Requires MongoEngine 0.8.6 or higher. """ -import six - from django.conf import settings -from mongoengine import DictField, Document, IntField, ReferenceField, \ - StringField, EmailField, BooleanField +from mongoengine import Document, ReferenceField from mongoengine.queryset import OperationError from social.utils import setting_name, module_member -from social.storage.django_orm import DjangoUserMixin, \ - DjangoAssociationMixin, \ - DjangoNonceMixin, \ - DjangoCodeMixin, \ - BaseDjangoStorage +from social.storage.django_orm import BaseDjangoStorage + +from social.storage.mongoengine_orm import MongoengineUserMixin, \ + MongoengineNonceMixin, \ + MongoengineAssociationMixin, \ + MongoengineCodeMixin UNUSABLE_PASSWORD = '!' # Borrowed from django 1.4 @@ -44,86 +41,28 @@ def _get_user_model(): USER_MODEL = _get_user_model() -class UserSocialAuth(Document, DjangoUserMixin): +class UserSocialAuth(Document, MongoengineUserMixin): """Social Auth association model""" user = ReferenceField(USER_MODEL) - provider = StringField(max_length=32) - uid = StringField(max_length=255, unique_with='provider') - extra_data = DictField() - - def str_id(self): - return str(self.id) - - @classmethod - def get_social_auth_for_user(cls, user, provider=None, id=None): - qs = cls.objects - if provider: - qs = qs.filter(provider=provider) - if id: - qs = qs.filter(id=id) - return qs.filter(user=user) - - @classmethod - def create_social_auth(cls, user, uid, provider): - if not isinstance(type(uid), six.string_types): - uid = str(uid) - return cls.objects.create(user=user, uid=uid, provider=provider) - - @classmethod - def username_max_length(cls): - username_field = cls.username_field() - field = getattr(UserSocialAuth.user_model(), username_field) - return field.max_length @classmethod def user_model(cls): return USER_MODEL - @classmethod - def create_user(cls, *args, **kwargs): - kwargs['password'] = UNUSABLE_PASSWORD - if 'email' in kwargs: - # Empty string makes email regex validation fail - kwargs['email'] = kwargs['email'] or None - return cls.user_model().create_user(*args, **kwargs) - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - if association_id is not None: - qs = cls.objects.filter(id__ne=association_id) - else: - qs = cls.objects.filter(provider__ne=backend_name) - qs = qs.filter(user=user) - - if hasattr(user, 'has_usable_password'): - valid_password = user.has_usable_password() - else: - valid_password = True - return valid_password or qs.count() > 0 - - -class Nonce(Document, DjangoNonceMixin): +class Nonce(Document, MongoengineNonceMixin): """One use numbers""" - server_url = StringField(max_length=255) - timestamp = IntField() - salt = StringField(max_length=40) + pass -class Association(Document, DjangoAssociationMixin): +class Association(Document, MongoengineAssociationMixin): """OpenId account association""" - server_url = StringField(max_length=255) - handle = StringField(max_length=255) - secret = StringField(max_length=255) # Stored base64 encoded - issued = IntField() - lifetime = IntField() - assoc_type = StringField(max_length=64) - - -class Code(Document, DjangoCodeMixin): - email = EmailField() - code = StringField(max_length=32) - verified = BooleanField(default=False) + pass + + +class Code(Document, MongoengineCodeMixin): + """Mail validation single one time use code""" + pass class DjangoStorage(BaseDjangoStorage): diff --git a/social/storage/base.py b/social/storage/base.py index 71afc0096..d32a054cb 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -87,6 +87,11 @@ def set_extra_data(self, extra_data=None): self.extra_data = extra_data return True + @classmethod + def clean_username(cls, value): + """Clean username removing any unsupported character""" + return CLEAN_USERNAME_REGEX.sub('', value) + @classmethod def changed(cls, user): """The given user instance is ready to be saved""" @@ -107,10 +112,6 @@ def username_max_length(cls): """Return the max length for username""" raise NotImplementedError('Implement in subclass') - @classmethod - def clean_username(cls, value): - return CLEAN_USERNAME_REGEX.sub('', value) - @classmethod def allowed_to_disconnect(cls, user, backend_name, association_id=None): """Return if it's safe to disconnect the social account for the From 9286ccea71ae8e4bb0dfe787e5178db7288673ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 14 Jun 2014 02:33:19 -0300 Subject: [PATCH 264/890] Move common fields to base class in sqlalchemy ORMs. --- examples/cherrypy_example/requirements.txt | 1 + examples/webpy_example/app.py | 1 - social/apps/cherrypy_app/models.py | 24 +-------- social/apps/flask_app/models.py | 32 ++--------- social/apps/pyramid_app/models.py | 32 ++--------- social/apps/tornado_app/models.py | 32 ++--------- social/apps/webpy_app/app.py | 5 +- social/apps/webpy_app/models.py | 62 +++++----------------- social/apps/webpy_app/utils.py | 2 +- social/storage/sqlalchemy_orm.py | 52 ++++++++++++++---- social/strategies/webpy_strategy.py | 2 +- 11 files changed, 77 insertions(+), 168 deletions(-) create mode 100644 examples/cherrypy_example/requirements.txt diff --git a/examples/cherrypy_example/requirements.txt b/examples/cherrypy_example/requirements.txt new file mode 100644 index 000000000..d71870692 --- /dev/null +++ b/examples/cherrypy_example/requirements.txt @@ -0,0 +1 @@ +cherrypy diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 88d687fc3..1136b7692 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -74,7 +74,6 @@ def GET(self): class done(social_app.BaseViewClass): - @psa() def GET(self): user = self.get_current_user() return render.done(user=user, backends=backends(user)) diff --git a/social/apps/cherrypy_app/models.py b/social/apps/cherrypy_app/models.py index 7c80da2dd..d0c7806b0 100644 --- a/social/apps/cherrypy_app/models.py +++ b/social/apps/cherrypy_app/models.py @@ -3,7 +3,6 @@ from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship -from sqlalchemy.schema import UniqueConstraint from sqlalchemy.ext.declarative import declarative_base from social.utils import setting_name, module_member @@ -11,7 +10,6 @@ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ BaseSQLAlchemyStorage -from social.apps.flask_app.fields import JSONType SocialBase = declarative_base() @@ -29,12 +27,7 @@ def _session(cls): class UserSocialAuth(CherryPySocialBase, SQLAlchemyUserMixin, SocialBase): """Social Auth association model""" - __tablename__ = 'social_auth_usersocialauth' - __table_args__ = (UniqueConstraint('provider', 'uid'),) - id = Column(Integer, primary_key=True) - provider = Column(String(32)) uid = Column(String(UID_LENGTH)) - extra_data = Column(JSONType) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) user = relationship(User, backref='social_auth') @@ -50,25 +43,12 @@ def user_model(cls): class Nonce(CherryPySocialBase, SQLAlchemyNonceMixin, SocialBase): """One use numbers""" - __tablename__ = 'social_auth_nonce' - __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - timestamp = Column(Integer) - salt = Column(String(40)) + pass class Association(CherryPySocialBase, SQLAlchemyAssociationMixin, SocialBase): """OpenId account association""" - __tablename__ = 'social_auth_association' - __table_args__ = (UniqueConstraint('server_url', 'handle'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - handle = Column(String(255)) - secret = Column(String(255)) # base64 encoded - issued = Column(Integer) - lifetime = Column(Integer) - assoc_type = Column(String(64)) + pass class CherryPyStorage(BaseSQLAlchemyStorage): diff --git a/social/apps/flask_app/models.py b/social/apps/flask_app/models.py index 422474dfc..c284d0911 100644 --- a/social/apps/flask_app/models.py +++ b/social/apps/flask_app/models.py @@ -1,15 +1,13 @@ """Flask SQLAlchemy ORM models for Social Auth""" from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, backref -from sqlalchemy.schema import UniqueConstraint from social.utils import setting_name, module_member from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage, \ - JSONType + BaseSQLAlchemyStorage class FlaskStorage(BaseSQLAlchemyStorage): @@ -31,12 +29,7 @@ def _session(cls): class UserSocialAuth(_AppSession, db.Model, SQLAlchemyUserMixin): """Social Auth association model""" - __tablename__ = 'social_auth_usersocialauth' - __table_args__ = (UniqueConstraint('provider', 'uid'),) - id = Column(Integer, primary_key=True) - provider = Column(String(32)) uid = Column(String(UID_LENGTH)) - extra_data = Column(JSONType) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) user = relationship(User, backref=backref('social_auth', @@ -52,31 +45,14 @@ def user_model(cls): class Nonce(_AppSession, db.Model, SQLAlchemyNonceMixin): """One use numbers""" - __tablename__ = 'social_auth_nonce' - __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - timestamp = Column(Integer) - salt = Column(String(40)) + pass class Association(_AppSession, db.Model, SQLAlchemyAssociationMixin): """OpenId account association""" - __tablename__ = 'social_auth_association' - __table_args__ = (UniqueConstraint('server_url', 'handle'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - handle = Column(String(255)) - secret = Column(String(255)) # base64 encoded - issued = Column(Integer) - lifetime = Column(Integer) - assoc_type = Column(String(64)) + pass class Code(_AppSession, db.Model, SQLAlchemyCodeMixin): - __tablename__ = 'social_auth_code' - __table_args__ = (UniqueConstraint('code', 'email'),) - id = Column(Integer, primary_key=True) - email = Column(String(200)) - code = Column(String(32), index=True) + pass # Set the references in the storage class FlaskStorage.user = UserSocialAuth diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py index bcd54939f..912cf0062 100644 --- a/social/apps/pyramid_app/models.py +++ b/social/apps/pyramid_app/models.py @@ -1,15 +1,13 @@ """Pyramid SQLAlchemy ORM models for Social Auth""" from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, backref -from sqlalchemy.schema import UniqueConstraint from social.utils import setting_name, module_member from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage, \ - JSONType + BaseSQLAlchemyStorage class PyramidStorage(BaseSQLAlchemyStorage): @@ -34,12 +32,7 @@ def _session(cls): class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin): """Social Auth association model""" - __tablename__ = 'social_auth_usersocialauth' - __table_args__ = (UniqueConstraint('provider', 'uid'),) - id = Column(Integer, primary_key=True) - provider = Column(String(32)) uid = Column(String(UID_LENGTH)) - extra_data = Column(JSONType) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) user = relationship(User, backref=backref('social_auth', @@ -55,31 +48,14 @@ def user_model(cls): class Nonce(_AppSession, Base, SQLAlchemyNonceMixin): """One use numbers""" - __tablename__ = 'social_auth_nonce' - __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - timestamp = Column(Integer) - salt = Column(String(40)) + pass class Association(_AppSession, Base, SQLAlchemyAssociationMixin): """OpenId account association""" - __tablename__ = 'social_auth_association' - __table_args__ = (UniqueConstraint('server_url', 'handle'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - handle = Column(String(255)) - secret = Column(String(255)) # base64 encoded - issued = Column(Integer) - lifetime = Column(Integer) - assoc_type = Column(String(64)) + pass class Code(_AppSession, Base, SQLAlchemyCodeMixin): - __tablename__ = 'social_auth_code' - __table_args__ = (UniqueConstraint('code', 'email'),) - id = Column(Integer, primary_key=True) - email = Column(String(200)) - code = Column(String(32), index=True) + pass # Set the references in the storage class PyramidStorage.user = UserSocialAuth diff --git a/social/apps/tornado_app/models.py b/social/apps/tornado_app/models.py index 40338066b..803d24ed0 100644 --- a/social/apps/tornado_app/models.py +++ b/social/apps/tornado_app/models.py @@ -1,15 +1,13 @@ """Tornado SQLAlchemy ORM models for Social Auth""" from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, backref -from sqlalchemy.schema import UniqueConstraint from social.utils import setting_name, module_member from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage, \ - JSONType + BaseSQLAlchemyStorage class TornadoStorage(BaseSQLAlchemyStorage): @@ -31,12 +29,7 @@ def _session(cls): class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin): """Social Auth association model""" - __tablename__ = 'social_auth_usersocialauth' - __table_args__ = (UniqueConstraint('provider', 'uid'),) - id = Column(Integer, primary_key=True) - provider = Column(String(32)) uid = Column(String(UID_LENGTH)) - extra_data = Column(JSONType) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) user = relationship(User, backref=backref('social_auth', @@ -52,31 +45,14 @@ def user_model(cls): class Nonce(_AppSession, Base, SQLAlchemyNonceMixin): """One use numbers""" - __tablename__ = 'social_auth_nonce' - __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - timestamp = Column(Integer) - salt = Column(String(40)) + pass class Association(_AppSession, Base, SQLAlchemyAssociationMixin): """OpenId account association""" - __tablename__ = 'social_auth_association' - __table_args__ = (UniqueConstraint('server_url', 'handle'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - handle = Column(String(255)) - secret = Column(String(255)) # base64 encoded - issued = Column(Integer) - lifetime = Column(Integer) - assoc_type = Column(String(64)) + pass class Code(_AppSession, Base, SQLAlchemyCodeMixin): - __tablename__ = 'social_auth_code' - __table_args__ = (UniqueConstraint('code', 'email'),) - id = Column(Integer, primary_key=True) - email = Column(String(200)) - code = Column(String(32), index=True) + pass # Set the references in the storage class TornadoStorage.user = UserSocialAuth diff --git a/social/apps/webpy_app/app.py b/social/apps/webpy_app/app.py index 4cfe26cc7..3a78b642a 100644 --- a/social/apps/webpy_app/app.py +++ b/social/apps/webpy_app/app.py @@ -1,7 +1,7 @@ import web from social.actions import do_auth, do_complete, do_disconnect -from social.apps.webpy_app.utils import psa +from social.apps.webpy_app.utils import psa, load_strategy urls = ( @@ -16,13 +16,14 @@ class BaseViewClass(object): def __init__(self, *args, **kwargs): self.session = web.web_session method = web.ctx.method == 'POST' and 'post' or 'get' + self.strategy = load_strategy() self.data = web.input(_method=method) super(BaseViewClass, self).__init__(*args, **kwargs) def get_current_user(self): if not hasattr(self, '_user'): if self.session.get('logged_in'): - self._user = self.backend.strategy.get_user( + self._user = self.strategy.get_user( self.session.get('user_id') ) else: diff --git a/social/apps/webpy_app/models.py b/social/apps/webpy_app/models.py index adfb3ce4c..68b91f720 100644 --- a/social/apps/webpy_app/models.py +++ b/social/apps/webpy_app/models.py @@ -3,7 +3,6 @@ from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship -from sqlalchemy.schema import UniqueConstraint from sqlalchemy.ext.declarative import declarative_base from social.utils import setting_name, module_member @@ -11,8 +10,7 @@ SQLAlchemyAssociationMixin, \ SQLAlchemyNonceMixin, \ SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage, \ - JSONType + BaseSQLAlchemyStorage SocialBase = declarative_base() @@ -21,14 +19,15 @@ User = module_member(web.config[setting_name('USER_MODEL')]) -class UserSocialAuth(SQLAlchemyUserMixin, SocialBase): +class WebpySocialBase(object): + @classmethod + def _session(cls): + return web.db_session + + +class UserSocialAuth(WebpySocialBase, SQLAlchemyUserMixin, SocialBase): """Social Auth association model""" - __tablename__ = 'social_auth_usersocialauth' - __table_args__ = (UniqueConstraint('provider', 'uid'),) - id = Column(Integer, primary_key=True) - provider = Column(String(32)) uid = Column(String(UID_LENGTH)) - extra_data = Column(JSONType) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) user = relationship(User, backref='social_auth') @@ -41,52 +40,19 @@ def username_max_length(cls): def user_model(cls): return User - @classmethod - def _session(cls): - return web.db_session - -class Nonce(SQLAlchemyNonceMixin, SocialBase): +class Nonce(WebpySocialBase, SQLAlchemyNonceMixin, SocialBase): """One use numbers""" - __tablename__ = 'social_auth_nonce' - __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - timestamp = Column(Integer) - salt = Column(String(40)) - - @classmethod - def _session(cls): - return web.db_session + pass -class Association(SQLAlchemyAssociationMixin, SocialBase): +class Association(WebpySocialBase, SQLAlchemyAssociationMixin, SocialBase): """OpenId account association""" - __tablename__ = 'social_auth_association' - __table_args__ = (UniqueConstraint('server_url', 'handle'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - handle = Column(String(255)) - secret = Column(String(255)) # base64 encoded - issued = Column(Integer) - lifetime = Column(Integer) - assoc_type = Column(String(64)) - - @classmethod - def _session(cls): - return web.db_session - + pass -class Code(SQLAlchemyCodeMixin, SocialBase): - __tablename__ = 'social_auth_code' - __table_args__ = (UniqueConstraint('code', 'email'),) - id = Column(Integer, primary_key=True) - email = Column(String(200)) - code = Column(String(32), index=True) - @classmethod - def _session(cls): - return web.db_session +class Code(WebpySocialBase, SQLAlchemyCodeMixin, SocialBase): + pass class WebpyStorage(BaseSQLAlchemyStorage): diff --git a/social/apps/webpy_app/utils.py b/social/apps/webpy_app/utils.py index 023bf7dfd..c42fe9f4b 100644 --- a/social/apps/webpy_app/utils.py +++ b/social/apps/webpy_app/utils.py @@ -38,7 +38,7 @@ def wrapper(self, backend, *args, **kwargs): uri = uri % {'backend': backend} self.strategy = load_strategy() self.backend = load_backend(self.strategy, backend, uri) - return func(self, backend=backend, *args, **kwargs) + return func(self, backend, *args, **kwargs) return wrapper return decorator diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index a0c1d0767..a88118622 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -5,11 +5,22 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.types import PickleType, Text +from sqlalchemy import Column, Integer, String +from sqlalchemy.schema import UniqueConstraint from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage +# JSON type field +class JSONType(PickleType): + impl = Text + + def __init__(self, *args, **kwargs): + kwargs['pickler'] = json + super(JSONType, self).__init__(*args, **kwargs) + + class SQLAlchemyMixin(object): COMMIT_SESSION = True @@ -38,6 +49,15 @@ def save(self): class SQLAlchemyUserMixin(SQLAlchemyMixin, UserMixin): """Social Auth association model""" + __tablename__ = 'social_auth_usersocialauth' + __table_args__ = (UniqueConstraint('provider', 'uid'),) + id = Column(Integer, primary_key=True) + provider = Column(String(32)) + extra_data = Column(JSONType) + uid = None + user_id = None + user = None + @classmethod def changed(cls, user): cls._save_instance(user) @@ -120,6 +140,13 @@ def create_social_auth(cls, user, uid, provider): class SQLAlchemyNonceMixin(SQLAlchemyMixin, NonceMixin): + __tablename__ = 'social_auth_nonce' + __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) + id = Column(Integer, primary_key=True) + server_url = Column(String(255)) + timestamp = Column(Integer) + salt = Column(String(40)) + @classmethod def use(cls, server_url, timestamp, salt): kwargs = {'server_url': server_url, 'timestamp': timestamp, @@ -131,6 +158,16 @@ def use(cls, server_url, timestamp, salt): class SQLAlchemyAssociationMixin(SQLAlchemyMixin, AssociationMixin): + __tablename__ = 'social_auth_association' + __table_args__ = (UniqueConstraint('server_url', 'handle'),) + id = Column(Integer, primary_key=True) + server_url = Column(String(255)) + handle = Column(String(255)) + secret = Column(String(255)) # base64 encoded + issued = Column(Integer) + lifetime = Column(Integer) + assoc_type = Column(String(64)) + @classmethod def store(cls, server_url, association): # Don't use get_or_create because issued cannot be null @@ -158,6 +195,12 @@ def remove(cls, ids_to_delete): class SQLAlchemyCodeMixin(SQLAlchemyMixin, CodeMixin): + __tablename__ = 'social_auth_code' + __table_args__ = (UniqueConstraint('code', 'email'),) + id = Column(Integer, primary_key=True) + email = Column(String(200)) + code = Column(String(32), index=True) + @classmethod def get_code(cls, code): return cls._query().filter(cls.code == code).first() @@ -172,12 +215,3 @@ class BaseSQLAlchemyStorage(BaseStorage): @classmethod def is_integrity_error(cls, exception): return exception.__class__ is IntegrityError - - -# JSON type field -class JSONType(PickleType): - impl = Text - - def __init__(self, *args, **kwargs): - kwargs['pickler'] = json - super(JSONType, self).__init__(*args, **kwargs) diff --git a/social/strategies/webpy_strategy.py b/social/strategies/webpy_strategy.py index 5b9a82b47..a6ae53824 100644 --- a/social/strategies/webpy_strategy.py +++ b/social/strategies/webpy_strategy.py @@ -27,7 +27,7 @@ def request_data(self, merge=True): return data def request_host(self): - return self.request.host + return web.ctx.host def redirect(self, url): return web.seeother(url) From 3fb842bf26b8525f8f9e55164ad5cc060b8d3c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 14 Jun 2014 06:00:14 -0300 Subject: [PATCH 265/890] Fix docstring --- social/apps/django_app/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index a20be1ba1..6f23a42b0 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -15,8 +15,7 @@ def auth(request, backend): @csrf_exempt @psa('social:complete') def complete(request, backend, *args, **kwargs): - """Authentication complete view, override this view if transaction - management doesn't suit your needs.""" + """Authentication complete view""" return do_complete(request.backend, _do_login, request.user, redirect_name=REDIRECT_FIELD_NAME, *args, **kwargs) From 9585309f57ef013704f5b0e7ac02025cf281b38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 14 Jun 2014 17:16:33 -0300 Subject: [PATCH 266/890] PEP8 --- social/apps/django_app/__init__.py | 3 ++- social/backends/mapmyfitness.py | 5 +++-- social/backends/stackoverflow.py | 11 +++++++---- social/tests/backends/test_kakao.py | 6 ++++-- social/tests/backends/test_mapmyfitness.py | 15 ++++++++++----- social/tests/backends/test_podio.py | 19 ++++++++++--------- social/tests/backends/test_taobao.py | 2 +- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/social/apps/django_app/__init__.py b/social/apps/django_app/__init__.py index 1f5b92f98..5225a4f5c 100644 --- a/social/apps/django_app/__init__.py +++ b/social/apps/django_app/__init__.py @@ -4,7 +4,8 @@ To use this: * Add 'social.apps.django_app.default' if using default ORM, or 'social.apps.django_app.me' if using mongoengine - * Add url('', include('social.apps.django_app.urls', namespace='social')) to urls.py + * Add url('', include('social.apps.django_app.urls', namespace='social')) to + urls.py * Define SOCIAL_AUTH_STORAGE and SOCIAL_AUTH_STRATEGY, default values: SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' diff --git a/social/backends/mapmyfitness.py b/social/backends/mapmyfitness.py index c1b714ce7..6c6e6a94c 100644 --- a/social/backends/mapmyfitness.py +++ b/social/backends/mapmyfitness.py @@ -9,7 +9,8 @@ class MapMyFitnessOAuth2(BaseOAuth2): """MapMyFitness OAuth authentication backend""" name = 'mapmyfitness' AUTHORIZATION_URL = 'https://www.mapmyfitness.com/v7.0/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://oauth2-api.mapmyapi.com/v7.0/oauth2/access_token' + ACCESS_TOKEN_URL = \ + 'https://oauth2-api.mapmyapi.com/v7.0/oauth2/access_token' REQUEST_TOKEN_METHOD = 'POST' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False @@ -45,4 +46,4 @@ def user_data(self, access_token, *args, **kwargs): 'Authorization': 'Bearer {0}'.format(access_token), 'Api-Key': key } - return self.get_json(url, headers=headers) + return self.get_json(url, headers=headers) diff --git a/social/backends/stackoverflow.py b/social/backends/stackoverflow.py index 1da1fa9ca..8b6cbbfa8 100644 --- a/social/backends/stackoverflow.py +++ b/social/backends/stackoverflow.py @@ -30,10 +30,13 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - return self.get_json('https://api.stackexchange.com/2.1/me', - params={'site': 'stackoverflow', - 'access_token': access_token, - 'key': self.setting('API_KEY')} + return self.get_json( + 'https://api.stackexchange.com/2.1/me', + params={ + 'site': 'stackoverflow', + 'access_token': access_token, + 'key': self.setting('API_KEY') + } )['items'][0] def request_access_token(self, *args, **kwargs): diff --git a/social/tests/backends/test_kakao.py b/social/tests/backends/test_kakao.py index c8abb6b51..c687cd781 100644 --- a/social/tests/backends/test_kakao.py +++ b/social/tests/backends/test_kakao.py @@ -14,8 +14,10 @@ class KakaoOAuth2Test(OAuth2Test): 'id': '101010101', 'properties': { 'nickname': 'foobar', - 'thumbnail_image': 'http://mud-kage.kakao.co.kr/14/dn/btqbh1AKmRf/ujlHpQhxtMSbhKrBisrxe1/o.jpg', - 'profile_image': 'http://mud-kage.kakao.co.kr/14/dn/btqbjCnl06Q/wbMJSVAUZB7lzSImgGdsoK/o.jpg' + 'thumbnail_image': 'http://mud-kage.kakao.co.kr/14/dn/btqbh1AKmRf/' + 'ujlHpQhxtMSbhKrBisrxe1/o.jpg', + 'profile_image': 'http://mud-kage.kakao.co.kr/14/dn/btqbjCnl06Q/' + 'wbMJSVAUZB7lzSImgGdsoK/o.jpg' } }) diff --git a/social/tests/backends/test_mapmyfitness.py b/social/tests/backends/test_mapmyfitness.py index b57bdcafe..714d4121b 100644 --- a/social/tests/backends/test_mapmyfitness.py +++ b/social/tests/backends/test_mapmyfitness.py @@ -33,12 +33,14 @@ class MapMyFitnessOAuth2Test(OAuth2Test): '_links': { 'stats': [ { - 'href': '/v7.0/user_stats/112233/?aggregate_by_period=month', + 'href': '/v7.0/user_stats/112233/?' + 'aggregate_by_period=month', 'id': '112233', 'name': 'month' }, { - 'href': '/v7.0/user_stats/112233/?aggregate_by_period=year', + 'href': '/v7.0/user_stats/112233/?' + 'aggregate_by_period=year', 'id': '112233', 'name': 'year' }, @@ -48,12 +50,14 @@ class MapMyFitnessOAuth2Test(OAuth2Test): 'name': 'day' }, { - 'href': '/v7.0/user_stats/112233/?aggregate_by_period=week', + 'href': '/v7.0/user_stats/112233/?' + 'aggregate_by_period=week', 'id': '112233', 'name': 'week' }, { - 'href': '/v7.0/user_stats/112233/?aggregate_by_period=lifetime', + 'href': '/v7.0/user_stats/112233/?' + 'aggregate_by_period=lifetime', 'id': '112233', 'name': 'lifetime' } @@ -109,7 +113,8 @@ class MapMyFitnessOAuth2Test(OAuth2Test): ], 'workouts': [ { - 'href': '/v7.0/workout/?user=112233&order_by=-start_datetime' + 'href': '/v7.0/workout/?user=112233&' + 'order_by=-start_datetime' } ], 'deactivation': [ diff --git a/social/tests/backends/test_podio.py b/social/tests/backends/test_podio.py index 3ac846154..f42752d80 100644 --- a/social/tests/backends/test_podio.py +++ b/social/tests/backends/test_podio.py @@ -26,15 +26,16 @@ class PodioOAuth2Test(OAuth2Test): 'timezone': 'Europe/Copenhagen', 'mail': 'foo@bar.com', 'mails': [ - {'disabled': False, - 'mail': 'foobar@example.com', - 'primary': False, - 'verified': True - }, - {'disabled': False, - 'mail': 'foo@bar.com', - 'primary': True, - 'verified': True + { + 'disabled': False, + 'mail': 'foobar@example.com', + 'primary': False, + 'verified': True + }, { + 'disabled': False, + 'mail': 'foo@bar.com', + 'primary': True, + 'verified': True } ], # more properties ... diff --git a/social/tests/backends/test_taobao.py b/social/tests/backends/test_taobao.py index b3d3eee1a..aaba5f385 100644 --- a/social/tests/backends/test_taobao.py +++ b/social/tests/backends/test_taobao.py @@ -10,7 +10,7 @@ class TaobaoOAuth2Test(OAuth2Test): access_token_body = json.dumps({ 'access_token': 'foobar', 'token_type': 'bearer' - }) + }) user_data_body = json.dumps({ 'w2_expires_in': 0, 'taobao_user_id': '1', From 280faef1777b7ff67088c237ff73de4f6e6e992f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 14 Jun 2014 22:45:07 -0300 Subject: [PATCH 267/890] Integrate flask app and flask mongoengine app --- docs/configuration/flask.rst | 12 ++--- examples/flask_example/__init__.py | 2 +- examples/flask_example/models/__init__.py | 2 +- examples/flask_me_example/__init__.py | 7 +-- examples/flask_me_example/models/__init__.py | 2 +- examples/flask_me_example/routes/__init__.py | 2 +- social/apps/flask_app/default/__init__.py | 0 social/apps/flask_app/{ => default}/models.py | 0 social/apps/flask_app/me/__init__.py | 0 .../{flask_me_app => flask_app/me}/models.py | 0 social/apps/flask_app/routes.py | 2 + social/apps/flask_app/utils.py | 4 +- social/apps/flask_me_app/__init__.py | 5 -- social/apps/flask_me_app/routes.py | 39 ---------------- social/apps/flask_me_app/template_filters.py | 25 ---------- social/apps/flask_me_app/utils.py | 46 ------------------- 16 files changed, 17 insertions(+), 131 deletions(-) create mode 100644 social/apps/flask_app/default/__init__.py rename social/apps/flask_app/{ => default}/models.py (100%) create mode 100644 social/apps/flask_app/me/__init__.py rename social/apps/{flask_me_app => flask_app/me}/models.py (100%) delete mode 100644 social/apps/flask_me_app/__init__.py delete mode 100644 social/apps/flask_me_app/routes.py delete mode 100644 social/apps/flask_me_app/template_filters.py delete mode 100644 social/apps/flask_me_app/utils.py diff --git a/docs/configuration/flask.rst b/docs/configuration/flask.rst index 7959dd777..bc364cf19 100644 --- a/docs/configuration/flask.rst +++ b/docs/configuration/flask.rst @@ -16,17 +16,15 @@ Enabling the application ------------------------ The applications define a `Flask Blueprint`_, which needs to be registered once -the Flask app is configured:: +the Flask app is configured by:: from social.apps.flask_app.routes import social_auth app.register_blueprint(social_auth) -For MongoEngine_ version:: +For MongoEngine_ you need this setting:: - from social.apps.flask_me_app.routes import social_auth - - app.register_blueprint(social_auth) + SOCIAL_AUTH_STORAGE = 'social.apps.flask_app.me.models.FlaskStorage' Models Setup @@ -37,13 +35,13 @@ because they need the reference to the current db instance and the User model used on your project (check *User model reference* below). Once the Flask app and the database are defined, call ``init_social`` to register the models:: - from social.apps.flask_app.models import init_social + from social.apps.flask_app.default.models import init_social init_social(app, db) For MongoEngine_:: - from social.apps.flask_me_app.models import init_social + from social.apps.flask_app.me.models import init_social init_social(app, db) diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py index db82c077f..f791441f2 100644 --- a/examples/flask_example/__init__.py +++ b/examples/flask_example/__init__.py @@ -7,8 +7,8 @@ sys.path.append('../..') from social.apps.flask_app.routes import social_auth -from social.apps.flask_app.models import init_social from social.apps.flask_app.template_filters import backends +from social.apps.flask_app.default.models import init_social # App app = Flask(__name__) diff --git a/examples/flask_example/models/__init__.py b/examples/flask_example/models/__init__.py index c2cec89cf..e4adb9daf 100644 --- a/examples/flask_example/models/__init__.py +++ b/examples/flask_example/models/__init__.py @@ -1,2 +1,2 @@ from flask_example.models import user -from social.apps.flask_app import models +from social.apps.flask_app.default import models diff --git a/examples/flask_me_example/__init__.py b/examples/flask_me_example/__init__.py index f99aa29a1..f838a206c 100644 --- a/examples/flask_me_example/__init__.py +++ b/examples/flask_me_example/__init__.py @@ -6,14 +6,15 @@ sys.path.append('../..') -from social.apps.flask_me_app.routes import social_auth -from social.apps.flask_me_app.models import init_social -from social.apps.flask_me_app.template_filters import backends +from social.apps.flask_app.routes import social_auth +from social.apps.flask_app.me.models import init_social +from social.apps.flask_app.template_filters import backends # App app = Flask(__name__) app.config.from_object('flask_me_example.settings') +app.debug = True try: app.config.from_object('flask_me_example.local_settings') diff --git a/examples/flask_me_example/models/__init__.py b/examples/flask_me_example/models/__init__.py index cec669418..6020112c3 100644 --- a/examples/flask_me_example/models/__init__.py +++ b/examples/flask_me_example/models/__init__.py @@ -1,2 +1,2 @@ from flask_me_example.models import user -from social.apps.flask_me_app import models +from social.apps.flask_app.me import models diff --git a/examples/flask_me_example/routes/__init__.py b/examples/flask_me_example/routes/__init__.py index 915d4931c..0d41bfd26 100644 --- a/examples/flask_me_example/routes/__init__.py +++ b/examples/flask_me_example/routes/__init__.py @@ -1,2 +1,2 @@ from flask_me_example.routes import main -from social.apps.flask_me_app import routes +from social.apps.flask_app import routes diff --git a/social/apps/flask_app/default/__init__.py b/social/apps/flask_app/default/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/social/apps/flask_app/models.py b/social/apps/flask_app/default/models.py similarity index 100% rename from social/apps/flask_app/models.py rename to social/apps/flask_app/default/models.py diff --git a/social/apps/flask_app/me/__init__.py b/social/apps/flask_app/me/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/social/apps/flask_me_app/models.py b/social/apps/flask_app/me/models.py similarity index 100% rename from social/apps/flask_me_app/models.py rename to social/apps/flask_app/me/models.py diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index b56dd0d04..78c51fe1a 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -26,6 +26,8 @@ def complete(backend, *args, **kwargs): @social_auth.route('/disconnect//', methods=('POST',)) @social_auth.route('/disconnect///', methods=('POST',)) +@social_auth.route('/disconnect///', + methods=('POST',)) @login_required @psa() def disconnect(backend, association_id=None): diff --git a/social/apps/flask_app/utils.py b/social/apps/flask_app/utils.py index fa424d77f..8510afa92 100644 --- a/social/apps/flask_app/utils.py +++ b/social/apps/flask_app/utils.py @@ -8,7 +8,7 @@ DEFAULTS = { - 'STORAGE': 'social.apps.flask_app.models.FlaskStorage', + 'STORAGE': 'social.apps.flask_app.default.models.FlaskStorage', 'STRATEGY': 'social.strategies.flask_strategy.FlaskStrategy' } @@ -25,7 +25,7 @@ def load_strategy(): return get_strategy(strategy, storage) -def load_backend(strategy, name, redirect_uri): +def load_backend(strategy, name, redirect_uri, *args, **kwargs): backends = get_helper('AUTHENTICATION_BACKENDS') Backend = get_backend(backends, name) return Backend(strategy=strategy, redirect_uri=redirect_uri) diff --git a/social/apps/flask_me_app/__init__.py b/social/apps/flask_me_app/__init__.py deleted file mode 100644 index 125e69e7c..000000000 --- a/social/apps/flask_me_app/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from social.strategies.utils import set_current_strategy_getter -from social.apps.flask_me_app.utils import load_strategy - - -set_current_strategy_getter(load_strategy) diff --git a/social/apps/flask_me_app/routes.py b/social/apps/flask_me_app/routes.py deleted file mode 100644 index 41af2c383..000000000 --- a/social/apps/flask_me_app/routes.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import g, Blueprint, request -from flask.ext.login import login_required, login_user - -from social.actions import do_auth, do_complete, do_disconnect -from social.apps.flask_me_app.utils import psa - - -social_auth = Blueprint('social', __name__) - - -@social_auth.route('/login//', methods=('GET', 'POST')) -@psa('social.complete') -def auth(backend): - return do_auth(g.backend) - - -@social_auth.route('/complete//', methods=('GET', 'POST')) -@psa('social.complete') -def complete(backend, *args, **kwargs): - """Authentication complete view, override this view if transaction - management doesn't suit your needs.""" - return do_complete(g.backend, login=do_login, user=g.user, - *args, **kwargs) - - -@social_auth.route('/disconnect//', methods=('POST',)) -@social_auth.route('/disconnect///', - methods=('POST',)) -@login_required -@psa() -def disconnect(backend, association_id=None): - """Disconnects given backend from current logged in user.""" - return do_disconnect(g.backend, g.user, association_id) - - -def do_login(backend, user, social_user): - return login_user(user, remember=request.cookies.get('remember') or - request.args.get('remember') or - request.form.get('remember') or False) diff --git a/social/apps/flask_me_app/template_filters.py b/social/apps/flask_me_app/template_filters.py deleted file mode 100644 index ec561cdbf..000000000 --- a/social/apps/flask_me_app/template_filters.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import g, request - -from social.backends.utils import user_backends_data -from social.apps.flask_me_app.utils import get_helper - - -def backends(): - """Load Social Auth current user data to context under the key 'backends'. - Will return the output of social.backends.utils.user_backends_data.""" - return { - 'backends': user_backends_data(g.user, - get_helper('AUTHENTICATION_BACKENDS'), - get_helper('STORAGE', do_import=True)) - } - - -def login_redirect(): - """Load current redirect to context.""" - value = request.form.get('next', '') or \ - request.args.get('next', '') - return { - 'REDIRECT_FIELD_NAME': 'next', - 'REDIRECT_FIELD_VALUE': value, - 'REDIRECT_QUERYSTRING': value and ('next=' + value) or '' - } diff --git a/social/apps/flask_me_app/utils.py b/social/apps/flask_me_app/utils.py deleted file mode 100644 index 1a86b00f5..000000000 --- a/social/apps/flask_me_app/utils.py +++ /dev/null @@ -1,46 +0,0 @@ -from functools import wraps - -from flask import current_app, url_for, g - -from social.utils import module_member, setting_name -from social.strategies.utils import get_strategy -from social.backends.utils import get_backend - - -DEFAULTS = { - 'STORAGE': 'social.apps.flask_me_app.models.FlaskStorage', - 'STRATEGY': 'social.strategies.flask_strategy.FlaskStrategy' -} - - -def get_helper(name, do_import=False): - config = current_app.config.get(setting_name(name), - DEFAULTS.get(name, None)) - return do_import and module_member(config) or config - - -def load_strategy(): - strategy = get_helper('STRATEGY') - storage = get_helper('STORAGE') - return get_strategy(strategy, storage) - - -def load_backend(strategy, name, redirect_uri): - backends = get_helper('AUTHENTICATION_BACKENDS') - Backend = get_backend(backends, name) - return Backend(strategy=strategy, redirect_uri=redirect_uri) - - -def psa(redirect_uri=None): - def decorator(func): - @wraps(func) - def wrapper(backend, *args, **kwargs): - uri = redirect_uri - if uri and not uri.startswith('/'): - uri = url_for(uri, backend=backend) - g.strategy = load_strategy() - g.backend = load_backend(g.strategy, backend, redirect_uri=uri, - *args, **kwargs) - return func(backend, *args, **kwargs) - return wrapper - return decorator From f104811b8e81d6856990cd0d9ad801c99162ab2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 16 Jun 2014 15:54:53 -0300 Subject: [PATCH 268/890] Fix key access on instagram backend Fixes #296 --- social/backends/instagram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index 63eaf12ab..881047872 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -16,10 +16,11 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Instagram account""" - username = response['user']['username'] - email = response['user'].get('email', '') + user = response['data'] + username = user['username'] + email = user.get('email', '') fullname, first_name, last_name = self.get_user_names( - response['user'].get('full_name', '') + user.get('full_name', '') ) return {'username': username, 'fullname': fullname, From fd917776b9e4f59235df75552b7ef6bea9dc5a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 16 Jun 2014 22:23:01 -0300 Subject: [PATCH 269/890] Improve django example application look --- .../django_example/example/app/decorators.py | 16 + examples/django_example/example/app/mail.py | 16 +- .../django_example/example/app/pipeline.py | 5 +- .../example/app/templatetags/__init__.py | 0 .../example/app/templatetags/backend_utils.py | 80 +++ examples/django_example/example/app/views.py | 49 +- .../example/templates/base.html | 15 - .../example/templates/done.html | 67 -- .../example/templates/email.html | 11 - .../example/templates/email_signup.html | 19 - .../example/templates/home.html | 642 ++++++++++++------ .../example/templates/username_signup.html | 19 - .../example/templates/validation_sent.html | 6 - examples/django_example/example/urls.py | 1 - social/pipeline/mail.py | 2 +- social/strategies/base.py | 4 +- 16 files changed, 570 insertions(+), 382 deletions(-) create mode 100644 examples/django_example/example/app/decorators.py create mode 100644 examples/django_example/example/app/templatetags/__init__.py create mode 100644 examples/django_example/example/app/templatetags/backend_utils.py delete mode 100644 examples/django_example/example/templates/base.html delete mode 100644 examples/django_example/example/templates/done.html delete mode 100644 examples/django_example/example/templates/email.html delete mode 100644 examples/django_example/example/templates/email_signup.html delete mode 100644 examples/django_example/example/templates/username_signup.html delete mode 100644 examples/django_example/example/templates/validation_sent.html diff --git a/examples/django_example/example/app/decorators.py b/examples/django_example/example/app/decorators.py new file mode 100644 index 000000000..2ba85b130 --- /dev/null +++ b/examples/django_example/example/app/decorators.py @@ -0,0 +1,16 @@ +from functools import wraps + +from django.template import RequestContext +from django.shortcuts import render_to_response + + +def render_to(tpl): + def decorator(func): + @wraps(func) + def wrapper(request, *args, **kwargs): + out = func(request, *args, **kwargs) + if isinstance(out, dict): + out = render_to_response(tpl, out, RequestContext(request)) + return out + return wrapper + return decorator diff --git a/examples/django_example/example/app/mail.py b/examples/django_example/example/app/mail.py index 293ec990f..4dd59b5a7 100644 --- a/examples/django_example/example/app/mail.py +++ b/examples/django_example/example/app/mail.py @@ -3,11 +3,11 @@ from django.core.urlresolvers import reverse -def send_validation(strategy, code): - url = reverse('social:complete', args=(strategy.backend.name,)) + \ - '?verification_code=' + code.code - send_mail('Validate your account', - 'Validate your account {0}'.format(url), - settings.EMAIL_FROM, - [code.email], - fail_silently=False) +def send_validation(strategy, backend, code): + url = '{0}?verification_code={1}'.format( + reverse('social:complete', args=(backend.name,)), + code.code + ) + url = strategy.request.build_absolute_uri(url) + send_mail('Validate your account', 'Validate your account {0}'.format(url), + settings.EMAIL_FROM, [code.email], fail_silently=False) diff --git a/examples/django_example/example/app/pipeline.py b/examples/django_example/example/app/pipeline.py index a1cb2a127..245e69cf2 100644 --- a/examples/django_example/example/app/pipeline.py +++ b/examples/django_example/example/app/pipeline.py @@ -8,7 +8,8 @@ def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): if kwargs.get('ajax') or user and user.email: return elif is_new and not details.get('email'): - if strategy.session_get('saved_email'): - details['email'] = strategy.session_pop('saved_email') + email = strategy.request_data().get('email') + if email: + details['email'] = email else: return redirect('require_email') diff --git a/examples/django_example/example/app/templatetags/__init__.py b/examples/django_example/example/app/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/django_example/example/app/templatetags/backend_utils.py b/examples/django_example/example/app/templatetags/backend_utils.py new file mode 100644 index 000000000..b4edfca86 --- /dev/null +++ b/examples/django_example/example/app/templatetags/backend_utils.py @@ -0,0 +1,80 @@ +import re + +from django import template + +from social.backends.oauth import OAuthAuth + + +register = template.Library() + +name_re = re.compile(r'([^O])Auth') + + +@register.filter +def backend_name(backend): + name = backend.__class__.__name__ + name = name.replace('OAuth', ' OAuth') + name = name.replace('OpenId', ' OpenId') + name = name.replace('Sandbox', '') + name = name_re.sub(r'\1 Auth', name) + return name + + +@register.filter +def backend_class(backend): + return backend.name.replace('-', ' ') + + +@register.filter +def icon_name(name): + return { + 'stackoverflow': 'stack-overflow', + 'google-oauth': 'google', + 'google-oauth2': 'google', + 'yahoo-oauth': 'yahoo', + 'facebook-app': 'facebook', + 'email': 'envelope', + 'vimeo': 'vimeo-square', + 'linkedin-oauth2': 'linkedin', + 'vk-oauth2': 'vk', + 'live': 'windows', + 'username': 'user', + }.get(name, name) + + +@register.filter +def social_backends(backends): + backends = [(name, backend) for name, backend in backends.items() + if name not in ['username', 'email']] + backends.sort(key=lambda b: b[0]) + return [backends[n:n + 10] for n in range(0, len(backends), 10)] + + +@register.filter +def legacy_backends(backends): + backends = [(name, backend) for name, backend in backends.items() + if name in ['username', 'email']] + backends.sort(key=lambda b: b[0]) + return backends + + +@register.filter +def oauth_backends(backends): + backends = [(name, backend) for name, backend in backends.items() + if issubclass(backend, OAuthAuth)] + backends.sort(key=lambda b: b[0]) + return backends + + +@register.simple_tag(takes_context=True) +def associated(context, backend): + user = context.get('user') + context['association'] = None + if user and user.is_authenticated(): + try: + context['association'] = user.social_auth.filter( + provider=backend.name + )[0] + except IndexError: + pass + return '' diff --git a/examples/django_example/example/app/views.py b/examples/django_example/example/app/views.py index c2e5c7e7a..2ed66540b 100644 --- a/examples/django_example/example/app/views.py +++ b/examples/django_example/example/app/views.py @@ -2,58 +2,59 @@ from django.conf import settings from django.http import HttpResponse, HttpResponseBadRequest -from django.template import RequestContext -from django.shortcuts import render_to_response, redirect +from django.shortcuts import redirect from django.contrib.auth.decorators import login_required from django.contrib.auth import logout as auth_logout, login from social.backends.oauth import BaseOAuth1, BaseOAuth2 from social.backends.google import GooglePlusAuth +from social.backends.utils import load_backends from social.apps.django_app.utils import psa +from example.app.decorators import render_to + def logout(request): """Logs out user""" auth_logout(request) - return render_to_response('home.html', {}, RequestContext(request)) + return redirect('/') + + +def context(**extra): + return dict({ + 'plus_id': getattr(settings, 'SOCIAL_AUTH_GOOGLE_PLUS_KEY', None), + 'plus_scope': ' '.join(GooglePlusAuth.DEFAULT_SCOPE), + 'available_backends': load_backends(settings.AUTHENTICATION_BACKENDS) + }, **extra) +@render_to('home.html') def home(request): """Home view, displays login mechanism""" if request.user.is_authenticated(): return redirect('done') - return render_to_response('home.html', { - 'plus_id': getattr(settings, 'SOCIAL_AUTH_GOOGLE_PLUS_KEY', None) - }, RequestContext(request)) + return context() @login_required +@render_to('home.html') def done(request): """Login complete view, displays user data""" - scope = ' '.join(GooglePlusAuth.DEFAULT_SCOPE) - return render_to_response('done.html', { - 'user': request.user, - 'plus_id': getattr(settings, 'SOCIAL_AUTH_GOOGLE_PLUS_KEY', None), - 'plus_scope': scope - }, RequestContext(request)) - - -def signup_email(request): - return render_to_response('email_signup.html', {}, RequestContext(request)) + return context() +@render_to('home.html') def validation_sent(request): - return render_to_response('validation_sent.html', { - 'email': request.session.get('email_validation_address') - }, RequestContext(request)) + return context( + validation_sent=True, + email=request.session.get('email_validation_address') + ) +@render_to('home.html') def require_email(request): - if request.method == 'POST': - request.session['saved_email'] = request.POST.get('email') - backend = request.session['partial_pipeline']['backend'] - return redirect('social:complete', backend=backend) - return render_to_response('email.html', RequestContext(request)) + backend = request.session['partial_pipeline']['backend'] + return context(email_required=True, backend=backend) @psa('social:complete') diff --git a/examples/django_example/example/templates/base.html b/examples/django_example/example/templates/base.html deleted file mode 100644 index ef432efdc..000000000 --- a/examples/django_example/example/templates/base.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - Social - - {% block head_scripts %}{% endblock %} - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/django_example/example/templates/done.html b/examples/django_example/example/templates/done.html deleted file mode 100644 index 42f793bc7..000000000 --- a/examples/django_example/example/templates/done.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -

          You are logged in as {{ user.username }}! (Logout)

          - -

          Associated:

          -{% for assoc in backends.associated %} -
          - {{ assoc.provider }} - {% csrf_token %} - - -
          -{% endfor %} - -

          Associate:

          -
            - {% for name in backends.not_associated %} -
          • - {{ name }} -
          • - {% endfor %} -
          - -{% if plus_id %} -
          - -
          -
          -{% endif %} -{% endblock %} - -{% block head_scripts %} -{% if plus_id %} - - - - - -{% endif %} -{% endblock %} diff --git a/examples/django_example/example/templates/email.html b/examples/django_example/example/templates/email.html deleted file mode 100644 index 0bf012be5..000000000 --- a/examples/django_example/example/templates/email.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
          - {% csrf_token %} - - - -
          -{% endblock %} diff --git a/examples/django_example/example/templates/email_signup.html b/examples/django_example/example/templates/email_signup.html deleted file mode 100644 index bb5a6ff9a..000000000 --- a/examples/django_example/example/templates/email_signup.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
          - {% csrf_token %} - - - - - - - - - -
          - -
          -{% endblock %} diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 189e8ae26..0b8ddf011 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -1,211 +1,439 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -Amazon OAuth2
          -Angel OAuth2
          -AOL OpenId
          -Appsfuel OAuth2
          -Beats OAuth2
          -Behance OAuth2
          -BelgiumEID OpenId
          -Bitbucket OAuth1
          -Box.net OAuth2
          -Clef OAuth
          -Coinbase OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Douban OAuth2
          -Dropbox OAuth1
          -Evernote OAuth (sandbox mode)
          -Facebook OAuth2
          -Facebook App
          -Fedora OpenId
          -Fitbit OAuth1
          -Flickr OAuth1
          -Foursquare OAuth2
          -Github OAuth2
          -Google OpenId
          -Google OAuth1
          -Google OAuth2
          -Instagram OAuth2
          -Jawbone OAuth2
          -LinkedIn OAuth1
          -LinkedIn OAuth2
          -Live OAuth2
          -Mail.ru OAuth2
          -Mendeley OAuth1
          -Mendeley OAuth2
          -Mixcloud OAuth2
          -Odnoklassniki OAuth2
          -OpenStreetMap OAuth1
          -OpenSUSE OpenId
          -Orkut OAuth
          -Podio OAuth2
          -Rdio OAuth1
          -Rdio OAuth2
          -Readability OAuth1
          -Reddit OAuth2
          -Runkeeper OAuth2
          -Skyrock OAuth1
          -Soundcloud OAuth2
          -Spotify OAuth2
          -Stackoverflow OAuth2
          -Steam OpenId
          -Stocktwits OAuth2
          -Stripe OAuth2
          -ThisIsMyJam OAuth1
          -Trello OAuth1
          -Tripit OAuth1
          -Tumblr OAuth1
          -Twilio
          -Twitter OAuth1
          -VK.com OAuth2
          -Weibo OAuth2
          -Xing OAuth1
          -Yahoo OpenId
          -Yahoo OAuth1
          -Yammer OAuth2
          -Yandex OAuth2 -TAOBAO OAuth2
          -Vimeo OAuth1
          -LastFM Auth
          - -Email Auth
          -Username Auth
          - -
          {% csrf_token %} -
          - - - -
          -
          - -
          {% csrf_token %} -
          - - - -
          -
          - -
          {% csrf_token %} - - Persona -
          - -
          -
          - - - - - - - - -

          -
          - - -
          - -{% if plus_id %} -
          {% csrf_token %} - - - -
          - -
          -
          -{% endif %} - -{% endblock %} - -{% block head_scripts %} - - - - - + + + + + - - - - - - - -{% endblock %} + + + diff --git a/examples/django_example/example/templates/username_signup.html b/examples/django_example/example/templates/username_signup.html deleted file mode 100644 index 343d30c01..000000000 --- a/examples/django_example/example/templates/username_signup.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
          - {% csrf_token %} - - - - - - - - - -
          - -
          -{% endblock %} diff --git a/examples/django_example/example/templates/validation_sent.html b/examples/django_example/example/templates/validation_sent.html deleted file mode 100644 index 6614e3e96..000000000 --- a/examples/django_example/example/templates/validation_sent.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -A email validation was sent to {{ email }}. -{% endblock %} diff --git a/examples/django_example/example/urls.py b/examples/django_example/example/urls.py index abe1a1396..354ab4a5d 100644 --- a/examples/django_example/example/urls.py +++ b/examples/django_example/example/urls.py @@ -7,7 +7,6 @@ urlpatterns = patterns('', url(r'^$', 'example.app.views.home'), url(r'^admin/', include(admin.site.urls)), - url(r'^signup-email/', 'example.app.views.signup_email'), url(r'^email-sent/', 'example.app.views.validation_sent'), url(r'^login/$', 'example.app.views.home'), url(r'^logout/$', 'example.app.views.logout'), diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index ac191718c..9fbb41645 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -14,7 +14,7 @@ def mail_validation(backend, details, *args, **kwargs): data['verification_code']): raise InvalidEmail(backend) else: - backend.strategy.send_email_validation(details['email']) + backend.strategy.send_email_validation(backend, details['email']) backend.strategy.session_set('email_validation_address', details['email']) return backend.strategy.redirect( diff --git a/social/strategies/base.py b/social/strategies/base.py index 5c40eb6d4..b25e214eb 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -126,11 +126,11 @@ def get_language(self): """Return current language""" return '' - def send_email_validation(self, email): + def send_email_validation(self, backend, email): email_validation = self.setting('EMAIL_VALIDATION_FUNCTION') send_email = module_member(email_validation) code = self.storage.code.make_code(email) - send_email(self, code) + send_email(self, backend, code) return code def validate_email(self, email, code): From 6abf9e3ff8de8e2ffec52c7d659958fedeef742a Mon Sep 17 00:00:00 2001 From: Nikolaev Andrey Date: Wed, 18 Jun 2014 20:22:24 +0600 Subject: [PATCH 270/890] It was impossible to change the version API Vkotnakte --- social/backends/vk.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/social/backends/vk.py b/social/backends/vk.py index 1a94b5874..f633f5f58 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -187,10 +187,8 @@ def vk_api(backend, method, data): http://goo.gl/yLcaa """ # We need to perform server-side call if no access_token + data['v'] = backend.setting('API_VERSION', '3.0') if not 'access_token' in data: - if not 'v' in data: - data['v'] = '3.0' - key, secret = backend.get_key_and_secret() if not 'api_id' in data: data['api_id'] = key From bfdfbeef8e36b55b1435741a0c8e065dedfbb463 Mon Sep 17 00:00:00 2001 From: Gabriel Le Breton Date: Wed, 18 Jun 2014 11:07:36 -0400 Subject: [PATCH 271/890] text should not go into code block --- docs/configuration/django.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index 83fdff763..9c03d562d 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -58,11 +58,11 @@ setting:: 'django.contrib.auth.backends.ModelBackend', ) - Take into account that backends **must** be defined in AUTHENTICATION_BACKENDS_ - or Django won't pick them when trying to authenticate the user. +Take into account that backends **must** be defined in AUTHENTICATION_BACKENDS_ +or Django won't pick them when trying to authenticate the user. - Don't miss ``django.contrib.auth.backends.ModelBackend`` if using ``django.contrib.auth`` - application or users won't be able to login by username / password method. +Don't miss ``django.contrib.auth.backends.ModelBackend`` if using ``django.contrib.auth`` +application or users won't be able to login by username / password method. URLs entries From 5cd3556d4c11664111691f8897a66e80a759e04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 18 Jun 2014 19:32:06 -0300 Subject: [PATCH 272/890] Useful debug pipeling function --- social/pipeline/debug.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 social/pipeline/debug.py diff --git a/social/pipeline/debug.py b/social/pipeline/debug.py new file mode 100644 index 000000000..754dc61ec --- /dev/null +++ b/social/pipeline/debug.py @@ -0,0 +1,9 @@ +from pprint import pprint + + +def debug(response, details, *args, **kwargs): + print('=' * 80) + pprint(response) + print('=' * 80) + pprint(details) + print('=' * 80) From a09ee028d4ab5ab90e75d5264e862729532e5cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 18 Jun 2014 19:57:16 -0300 Subject: [PATCH 273/890] Initial work towards OpenIdConnect. Refs #300. Refs #284 --- .../example/app/templatetags/backend_utils.py | 1 + examples/django_example/example/settings.py | 4 ++- social/backends/google.py | 29 ++++++++++++++----- social/backends/open_id.py | 6 ++++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/examples/django_example/example/app/templatetags/backend_utils.py b/examples/django_example/example/app/templatetags/backend_utils.py index b4edfca86..99abf72a8 100644 --- a/examples/django_example/example/app/templatetags/backend_utils.py +++ b/examples/django_example/example/app/templatetags/backend_utils.py @@ -31,6 +31,7 @@ def icon_name(name): 'stackoverflow': 'stack-overflow', 'google-oauth': 'google', 'google-oauth2': 'google', + 'google-openidconnect': 'google', 'yahoo-oauth': 'yahoo', 'facebook-app': 'facebook', 'email': 'envelope', diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 878e26f64..62daabf7c 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -146,6 +146,7 @@ 'social.backends.google.GoogleOAuth2', 'social.backends.google.GoogleOpenId', 'social.backends.google.GooglePlusAuth', + 'social.backends.google.GoogleOpenIdConnect', 'social.backends.instagram.InstagramOAuth2', 'social.backends.jawbone.JawboneOAuth2', 'social.backends.linkedin.LinkedinOAuth', @@ -224,7 +225,8 @@ 'social.pipeline.user.create_user', 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details' + 'social.pipeline.user.user_details', + #'social.pipeline.debug.debug' ) # SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['first_name', 'last_name', 'email', diff --git a/social/backends/google.py b/social/backends/google.py index 33428b476..3913c19ed 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -4,7 +4,7 @@ """ from requests import HTTPError -from social.backends.open_id import OpenIdAuth +from social.backends.open_id import OpenIdAuth, OpenIdConnectAuth from social.backends.oauth import BaseOAuth2, BaseOAuth1 from social.exceptions import AuthMissingParameter, AuthCanceled @@ -25,19 +25,21 @@ def get_user_details(self, response): email = response['emails'][0]['value'] else: email = '' - if self.setting('USE_DEPRECATED_API', False): - name, given_name, family_name = ( - response.get('name', ''), - response.get('given_name', ''), - response.get('family_name', '') - ) - else: + + if isinstance(response.get('name'), dict): names = response.get('name') or {} name, given_name, family_name = ( response.get('displayName', ''), names.get('givenName', ''), names.get('familyName', '') ) + else: + name, given_name, family_name = ( + response.get('name', ''), + response.get('given_name', ''), + response.get('family_name', '') + ) + fullname, first_name, last_name = self.get_user_names( name, given_name, family_name ) @@ -199,3 +201,14 @@ def get_user_id(self, details, response): http://axschema.org/contact/email """ return details['email'] + + +class GoogleOpenIdConnect(GoogleOAuth2, OpenIdConnectAuth): + name = 'google-openidconnect' + + def user_data(self, access_token, *args, **kwargs): + """Return user data from Google API""" + return self.get_json( + 'https://www.googleapis.com/plus/v1/people/me/openIdConnect', + params={'access_token': access_token, 'alt': 'json'} + ) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 7db67d05e..c966a8259 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -6,6 +6,7 @@ from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ AuthUnknownError, AuthMissingParameter from social.backends.base import BaseAuth +from social.backends.oauth import BaseOAuth2 # OpenID configuration @@ -249,3 +250,8 @@ def openid_url(self): return self.data[OPENID_ID_FIELD] else: raise AuthMissingParameter(self, OPENID_ID_FIELD) + + +class OpenIdConnectAuth(BaseOAuth2): + DEFAULT_SCOPE = ['openid'] + EXTRA_DATA = ['id_token', 'refresh_token'] From ded008662e98cae053afa31a4dd637f151de2aba Mon Sep 17 00:00:00 2001 From: Avi Alkalay Date: Sat, 21 Jun 2014 22:27:27 -0300 Subject: [PATCH 274/890] The Moves app backend --- social/backends/moves.py | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 social/backends/moves.py diff --git a/social/backends/moves.py b/social/backends/moves.py new file mode 100644 index 000000000..00aba02bf --- /dev/null +++ b/social/backends/moves.py @@ -0,0 +1,46 @@ +""" +Moves OAuth2 backend, docs at: + https://dev.moves-app.com/docs/authentication + +Written by Avi Alkalay +Certified to work with Django 1.6 +""" +from social.backends.oauth import BaseOAuth2 + + +class MovesOAuth2(BaseOAuth2): + """Moves OAuth authentication backend""" + name = 'moves' + + # From https://dev.moves-app.com/docs/authentication#authorization + AUTHORIZATION_URL = 'https://api.moves-app.com/oauth/v1/authorize' + ACCESS_TOKEN_URL = 'https://api.moves-app.com/oauth/v1/access_token?grant_type=authorization_code' + REFRESH_TOKEN_URL = 'https://api.moves-app.com/oauth/v1/access_token?grant_type=refresh_token' + + ID_KEY = 'user_id' + REDIRECT_STATE = True + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ' ' + EXTRA_DATA = [ + ('refresh_token', 'refresh_token', True), + ('expires_in', 'expires'), + ('firstDate', 'firstdate') + ] + + def get_user_details(self, response): + """Return user details Moves account""" + return {'username': response.get('user_id')} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + params = self.setting('PROFILE_EXTRA_PARAMS', {}) + params['access_token'] = access_token + return self.get_json('https://api.moves-app.com/api/1.1/user/profile', + params=params) + + def refresh_token(self, token, *args, **kwargs): + params = self.refresh_token_params(token, *args, **kwargs) + request = self.request(self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL, + data=params, headers=self.auth_headers(), + method='POST') + return self.process_refresh_token_response(request, *args, **kwargs) From e18c061caa51551f40df105d772dfd1b6b2596ca Mon Sep 17 00:00:00 2001 From: Avi Alkalay Date: Sun, 22 Jun 2014 13:23:50 -0300 Subject: [PATCH 275/890] user first_date doesn't belong here --- social/backends/moves.py | 1 - 1 file changed, 1 deletion(-) diff --git a/social/backends/moves.py b/social/backends/moves.py index 00aba02bf..6f681e3b2 100644 --- a/social/backends/moves.py +++ b/social/backends/moves.py @@ -24,7 +24,6 @@ class MovesOAuth2(BaseOAuth2): EXTRA_DATA = [ ('refresh_token', 'refresh_token', True), ('expires_in', 'expires'), - ('firstDate', 'firstdate') ] def get_user_details(self, response): From c3df2f0a14779bc87618df9961abf3364ca2ae3e Mon Sep 17 00:00:00 2001 From: Roman Levin Date: Sun, 22 Jun 2014 22:32:30 +0200 Subject: [PATCH 276/890] Add note about access_type in docs --- docs/backends/google.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 9eefb67fa..2806b19cb 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -168,6 +168,17 @@ or:: depending on the backends in use. +Refresh Tokens +-------------- + +To get an OAuth2 refresh token along with the access token, you must pass an extra argument: ``access_type=offline``. +To do this with Google+ sign-in:: + + SOCIAL_AUTH_GOOGLE_PLUS_AUTH_EXTRA_ARGUMENTS = { + 'access_type': 'offline' + } + + Scopes deprecation ------------------ From 8d89e0b4eee9e398797ca97e73c96bcaf992a9a3 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Tue, 24 Jun 2014 04:59:43 -0400 Subject: [PATCH 277/890] Update link to Django example in documentation. Change link to Django example urls.py so it leads to the right file (and not a 404 error page). --- docs/configuration/porting_from_dsa.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/porting_from_dsa.rst b/docs/configuration/porting_from_dsa.rst index 665e7db89..42f4e5d13 100644 --- a/docs/configuration/porting_from_dsa.rst +++ b/docs/configuration/porting_from_dsa.rst @@ -113,5 +113,5 @@ means to force the user to login again. .. _django-social-auth: https://github.com/omab/django-social-auth .. _python-social-auth: https://github.com/omab/python-social-auth -.. _example app: https://github.com/omab/python-social-auth/blob/master/examples/django_example/dj/urls.py#L7 +.. _example app: https://github.com/omab/python-social-auth/blob/master/examples/django_example/example/urls.py#L17 .. _Facebook OAuth2 backend: https://github.com/omab/python-social-auth/blob/master/social/backends/facebook.py#L29 From db9f4add3a4bf58cf82f45f206e73cac96d10697 Mon Sep 17 00:00:00 2001 From: Antony Seedhouse Date: Wed, 25 Jun 2014 14:35:47 +0200 Subject: [PATCH 278/890] Update django_orm.py Make the get_user method generic --- social/storage/django_orm.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 9c9505a5f..bd6c7c3ce 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -59,9 +59,11 @@ def create_user(cls, *args, **kwargs): return cls.user_model().objects.create_user(*args, **kwargs) @classmethod - def get_user(cls, pk): + def get_user(cls, pk, **kwargs): + if pk: + kwargs = {'pk': pk} try: - return cls.user_model().objects.get(pk=pk) + return cls.user_model().objects.get(**kwargs) except cls.user_model().DoesNotExist: return None From a794aed8b30e4d12277fd526c1a0d68bf1aca336 Mon Sep 17 00:00:00 2001 From: Antony Seedhouse Date: Wed, 25 Jun 2014 16:10:40 +0200 Subject: [PATCH 279/890] Update django_orm.py pk parameter requires default value --- social/storage/django_orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index bd6c7c3ce..02d233ab4 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -59,7 +59,7 @@ def create_user(cls, *args, **kwargs): return cls.user_model().objects.create_user(*args, **kwargs) @classmethod - def get_user(cls, pk, **kwargs): + def get_user(cls, pk=None, **kwargs): if pk: kwargs = {'pk': pk} try: From 4ccd4f2c897a6efb3ea175bf7f982e076d3bc323 Mon Sep 17 00:00:00 2001 From: David Henderson Date: Thu, 26 Jun 2014 09:49:45 +0100 Subject: [PATCH 280/890] Reinstated get_user_id override - so that we can pull from the details rather than the response --- social/backends/exacttarget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/social/backends/exacttarget.py b/social/backends/exacttarget.py index c7647b51b..6da9f7498 100644 --- a/social/backends/exacttarget.py +++ b/social/backends/exacttarget.py @@ -23,6 +23,11 @@ def get_user_details(self, response): user['username'] = user['email'] return user + + def get_user_id(self, details, response): + """Create a user ID from the ET user ID. Uses details rather than the default response""" + return "exacttarget_%s" % details.get('id') + def uses_redirect(self): return False From 173639bb24bf8ab16327e245310dce6baaba6457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 30 Jun 2014 02:36:03 -0300 Subject: [PATCH 281/890] Tox runner with pyenv support --- run_tox.sh | 4 ++++ tox.ini | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100755 run_tox.sh diff --git a/run_tox.sh b/run_tox.sh new file mode 100755 index 000000000..ec3bbe0d9 --- /dev/null +++ b/run_tox.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +which pyenv && eval "$(pyenv init -)" +tox diff --git a/tox.ini b/tox.ini index 9671e3968..f5ea73d55 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py33, doc +envlist = py26, py27, py33, py34, doc [testenv] commands = nosetests --where=social/tests --stop From 78bd08e5c339c0b0bcf55661b05ef0ed2ef717ed Mon Sep 17 00:00:00 2001 From: davidhubbard Date: Tue, 1 Jul 2014 00:23:20 -0600 Subject: [PATCH 282/890] override request() to fix "429 Too Many Requests" override request so User-Agent does not get lumped in with all urllib hits on reddit (results in "429 Too Many Requests") http://stackoverflow.com/questions/13213048/urllib2-http-error-429 --- social/backends/reddit.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/social/backends/reddit.py b/social/backends/reddit.py index 96be7f811..00afba194 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -49,3 +49,13 @@ def refresh_token_params(self, token, redirect_uri=None, *args, **kwargs): params = super(RedditOAuth2, self).refresh_token_params(token) params['redirect_uri'] = self.redirect_uri or redirect_uri return params + + def request(self, url, method='GET', *args, **kwargs): + """override request so User-Agent does not get lumped in with all urllib hits on reddit (results in "429 Too Many Requests") + http://stackoverflow.com/questions/13213048/urllib2-http-error-429""" + ua = 'python-social-auth-' + sys.modules['social'].__version__ + if 'headers' not in kwargs: + kwargs.set('headers', {}) + if 'User-Agent' not in kwargs['headers']: + kwargs['headers']['User-Agent'] = ua + return super(RedditOAuth2, self).request(url, method, *args, **kwargs) From d8dade7f1032a9618bf937f8e70263f2f5e858fc Mon Sep 17 00:00:00 2001 From: davidhubbard Date: Tue, 1 Jul 2014 00:59:17 -0600 Subject: [PATCH 283/890] fix PR #317 --- social/backends/reddit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/reddit.py b/social/backends/reddit.py index 00afba194..d7eaa1219 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -5,6 +5,7 @@ import base64 from social.backends.oauth import BaseOAuth2 +import sys class RedditOAuth2(BaseOAuth2): From 85abe96ead4f035fc64fede5c8a9a6735acb3ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Slint=C3=A1k?= Date: Tue, 1 Jul 2014 12:52:01 +0200 Subject: [PATCH 284/890] Added Django 1.7 migrations --- .../default/migrations/0001_initial.py | 81 +++++++++++++++++++ .../django_app/default/migrations/__init__.py | 0 2 files changed, 81 insertions(+) create mode 100644 social/apps/django_app/default/migrations/0001_initial.py create mode 100644 social/apps/django_app/default/migrations/__init__.py diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py new file mode 100644 index 000000000..387f73c0b --- /dev/null +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import social.apps.django_app.default.fields +from django.conf import settings +import social.storage.django_orm + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Association', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('server_url', models.CharField(max_length=255)), + ('handle', models.CharField(max_length=255)), + ('secret', models.CharField(max_length=255)), + ('issued', models.IntegerField()), + ('lifetime', models.IntegerField()), + ('assoc_type', models.CharField(max_length=64)), + ], + options={ + 'db_table': 'social_auth_association', + }, + bases=(models.Model, social.storage.django_orm.DjangoAssociationMixin), + ), + migrations.CreateModel( + name='Code', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('email', models.EmailField(max_length=75)), + ('code', models.CharField(db_index=True, max_length=32)), + ('verified', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'social_auth_code', + }, + bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), + ), + migrations.AlterUniqueTogether( + name='code', + unique_together=set([('email', 'code')]), + ), + migrations.CreateModel( + name='Nonce', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('server_url', models.CharField(max_length=255)), + ('timestamp', models.IntegerField()), + ('salt', models.CharField(max_length=65)), + ], + options={ + 'db_table': 'social_auth_nonce', + }, + bases=(models.Model, social.storage.django_orm.DjangoNonceMixin), + ), + migrations.CreateModel( + name='UserSocialAuth', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('provider', models.CharField(max_length=32)), + ('uid', models.CharField(max_length=255)), + ('extra_data', social.apps.django_app.default.fields.JSONField(default='{}')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'social_auth_usersocialauth', + }, + bases=(models.Model, social.storage.django_orm.DjangoUserMixin), + ), + migrations.AlterUniqueTogether( + name='usersocialauth', + unique_together=set([('provider', 'uid')]), + ), + ] diff --git a/social/apps/django_app/default/migrations/__init__.py b/social/apps/django_app/default/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb From be1603887e3313db57cfa3d8eb545c3c6d38aaa5 Mon Sep 17 00:00:00 2001 From: Harz-FEAR Date: Fri, 4 Jul 2014 19:01:45 +0200 Subject: [PATCH 285/890] fix for AssertionError in pyramid fix for "AssertionError: Transaction must be committed using the transaction manager" sqlalchemy commits in Pyramid use the transaction manager --- social/storage/sqlalchemy_orm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index a88118622..f1934c434 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -83,7 +83,11 @@ def allowed_to_disconnect(cls, user, backend_name, association_id=None): @classmethod def disconnect(cls, entry): cls._session().delete(entry) - cls._session().commit() + try: + cls._session().commit() + except AssertionError: + import transaction + transaction.commit() @classmethod def user_query(cls): From 19952266e7b27d1464cc3f81ef665384b7bff8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 7 Jul 2014 10:04:59 -0300 Subject: [PATCH 286/890] Make user-agent setting available for all backends. Refs #317 --- social/backends/base.py | 8 +++++++- social/backends/reddit.py | 12 +----------- social/utils.py | 7 +++++++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/social/backends/base.py b/social/backends/base.py index 41da87c5f..607ec0315 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -1,6 +1,6 @@ from requests import request, ConnectionError -from social.utils import module_member, parse_qs +from social.utils import module_member, parse_qs, user_agent from social.exceptions import AuthFailed @@ -12,6 +12,7 @@ class BaseAuth(object): ID_KEY = None EXTRA_DATA = None REQUIRES_EMAIL_VALIDATION = False + SEND_USER_AGENT = False def __init__(self, strategy=None, redirect_uri=None): self.strategy = strategy @@ -207,8 +208,13 @@ def uses_redirect(self): return True def request(self, url, method='GET', *args, **kwargs): + kwargs.setdefault('headers', {}) kwargs.setdefault('timeout', self.setting('REQUESTS_TIMEOUT') or self.setting('URLOPEN_TIMEOUT')) + + if self.SEND_USER_AGENT and 'User-Agent' not in kwargs['headers']: + kwargs['headers']['User-Agent'] = user_agent() + try: response = request(method, url, *args, **kwargs) except ConnectionError as err: diff --git a/social/backends/reddit.py b/social/backends/reddit.py index d7eaa1219..e40598e66 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -5,7 +5,6 @@ import base64 from social.backends.oauth import BaseOAuth2 -import sys class RedditOAuth2(BaseOAuth2): @@ -18,6 +17,7 @@ class RedditOAuth2(BaseOAuth2): REDIRECT_STATE = False SCOPE_SEPARATOR = ',' DEFAULT_SCOPE = ['identity'] + SEND_USER_AGENT = True EXTRA_DATA = [ ('id', 'id'), ('link_karma', 'link_karma'), @@ -50,13 +50,3 @@ def refresh_token_params(self, token, redirect_uri=None, *args, **kwargs): params = super(RedditOAuth2, self).refresh_token_params(token) params['redirect_uri'] = self.redirect_uri or redirect_uri return params - - def request(self, url, method='GET', *args, **kwargs): - """override request so User-Agent does not get lumped in with all urllib hits on reddit (results in "429 Too Many Requests") - http://stackoverflow.com/questions/13213048/urllib2-http-error-429""" - ua = 'python-social-auth-' + sys.modules['social'].__version__ - if 'headers' not in kwargs: - kwargs.set('headers', {}) - if 'User-Agent' not in kwargs['headers']: - kwargs['headers']['User-Agent'] = ua - return super(RedditOAuth2, self).request(url, method, *args, **kwargs) diff --git a/social/utils.py b/social/utils.py index c1053349a..0ceb54fe8 100644 --- a/social/utils.py +++ b/social/utils.py @@ -4,6 +4,8 @@ import collections import six +import social + from social.p3 import urlparse, urlunparse, urlencode, \ parse_qs as battery_parse_qs @@ -22,6 +24,11 @@ def module_member(name): return getattr(module, member) +def user_agent(): + """Builds a simple User-Agent string to send in requests""" + return 'python-social-auth-' + social.__version__ + + def url_add_parameters(url, params): """Adds parameters to URL, parameter will be repeated if already present""" if params: From 47652671422467a947596d2177d584d352996710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 7 Jul 2014 10:21:22 -0300 Subject: [PATCH 287/890] Document django session migration script when moving from django-social-auth to python-social-auth. Refs #320 --- docs/configuration/porting_from_dsa.rst | 36 ++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/configuration/porting_from_dsa.rst b/docs/configuration/porting_from_dsa.rst index 42f4e5d13..aea7e72dc 100644 --- a/docs/configuration/porting_from_dsa.rst +++ b/docs/configuration/porting_from_dsa.rst @@ -106,12 +106,40 @@ changes. Examples of the new import paths:: Session ------- -Django stores the last authentication backend used in the user session, this -can cause import troubles when porting since the old import paths aren't valid -anymore. Sadly so far the only solution is to clean the sessions content, that -means to force the user to login again. +Django stores the last authentication backend used in the user session as an +import path, this can cause import troubles when porting since the old import +paths aren't valid anymore. Some solutions to this problem are: + +1. Clean the session and force the users to login again in your site + +2. Run a migration script that will update the authentication backend session + value for each session in your database. This implies figuring out the new + import path for each backend you have configured, which is the value used in + ``AUTHENTICATION_BACKENDS`` setting. + + `@tomgruner`_ created a Gist here_ that updates the value just for Facebook + backend. A ``template`` for this script would look like this:: + + from django.contrib.sessions.models import Session + + BACKENDS = { + 'social_auth.backends.facebook.FacebookBackend': 'social.backends.facebook.FacebookOAuth2' + } + + for sess in Session.objects.iterator(): + session_dict = sess.get_decoded() + + if '_auth_user_backend' in session_dict.keys(): + # Change old backend import path from new backend import path + if session_dict['_auth_user_backend'].startswith('social_auth'): + session_dict['_auth_user_backend'] = BACKENDS[session_dict['_auth_user_backend']] + new_sess = Session.objects.save(sess.session_key, session_dict, sess.expire_date) + print 'New session saved {}'.format(new_sess.pk) + .. _django-social-auth: https://github.com/omab/django-social-auth .. _python-social-auth: https://github.com/omab/python-social-auth .. _example app: https://github.com/omab/python-social-auth/blob/master/examples/django_example/example/urls.py#L17 .. _Facebook OAuth2 backend: https://github.com/omab/python-social-auth/blob/master/social/backends/facebook.py#L29 +.. _@tomgruner: https://github.com/tomgruner +.. _here: https://gist.github.com/tomgruner/5ce8bb1f4c55d17b5b25 From 0b5473b4ff390519af502276f90a2e72d9664365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 7 Jul 2014 10:42:50 -0300 Subject: [PATCH 288/890] Simple makefile for local tasks --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..04561bd5e --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +docs: + sphinx-build docs/ docs/_build/ + +site: docs + rsync -avkz site/ tarf:sites/psa/ + +.PHONY: site docs From f2bc7700efd475681593c4433bbe7dca5312ffca Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Mon, 7 Jul 2014 10:51:08 -0400 Subject: [PATCH 289/890] Use South instead of Django 1.7 migrations. --- requirements.txt | 1 + setup.py | 2 +- .../default/migrations/0001_initial.py | 228 ++++++++++++------ 3 files changed, 150 insertions(+), 81 deletions(-) diff --git a/requirements.txt b/requirements.txt index fa8c222ef..80f3d7145 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.0 six>=1.2.0 +South==0.8.4 diff --git a/setup.py b/setup.py index 2b5aabb6e..faccfcbe7 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0'] +requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'South>=0.8.4'] if PY3: requires += ['python3-openid>=3.0.1', 'requests-oauthlib>=0.3.0,<0.3.2'] diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 387f73c0b..4ae51a176 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -1,81 +1,149 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import social.apps.django_app.default.fields -from django.conf import settings -import social.storage.django_orm - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Association', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('server_url', models.CharField(max_length=255)), - ('handle', models.CharField(max_length=255)), - ('secret', models.CharField(max_length=255)), - ('issued', models.IntegerField()), - ('lifetime', models.IntegerField()), - ('assoc_type', models.CharField(max_length=64)), - ], - options={ - 'db_table': 'social_auth_association', - }, - bases=(models.Model, social.storage.django_orm.DjangoAssociationMixin), - ), - migrations.CreateModel( - name='Code', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('email', models.EmailField(max_length=75)), - ('code', models.CharField(db_index=True, max_length=32)), - ('verified', models.BooleanField(default=False)), - ], - options={ - 'db_table': 'social_auth_code', - }, - bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), - ), - migrations.AlterUniqueTogether( - name='code', - unique_together=set([('email', 'code')]), - ), - migrations.CreateModel( - name='Nonce', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('server_url', models.CharField(max_length=255)), - ('timestamp', models.IntegerField()), - ('salt', models.CharField(max_length=65)), - ], - options={ - 'db_table': 'social_auth_nonce', - }, - bases=(models.Model, social.storage.django_orm.DjangoNonceMixin), - ), - migrations.CreateModel( - name='UserSocialAuth', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('provider', models.CharField(max_length=32)), - ('uid', models.CharField(max_length=255)), - ('extra_data', social.apps.django_app.default.fields.JSONField(default='{}')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'social_auth_usersocialauth', - }, - bases=(models.Model, social.storage.django_orm.DjangoUserMixin), - ), - migrations.AlterUniqueTogether( - name='usersocialauth', - unique_together=set([('provider', 'uid')]), - ), - ] +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'UserSocialAuth' + db.create_table('social_auth_usersocialauth', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm['auth.User'])), + ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), + )) + db.send_create_signal(u'default', ['UserSocialAuth']) + + # Adding unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + db.create_unique('social_auth_usersocialauth', ['provider', 'uid']) + + # Adding model 'Nonce' + db.create_table('social_auth_nonce', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('timestamp', self.gf('django.db.models.fields.IntegerField')()), + ('salt', self.gf('django.db.models.fields.CharField')(max_length=65)), + )) + db.send_create_signal(u'default', ['Nonce']) + + # Adding model 'Association' + db.create_table('social_auth_association', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('handle', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('issued', self.gf('django.db.models.fields.IntegerField')()), + ('lifetime', self.gf('django.db.models.fields.IntegerField')()), + ('assoc_type', self.gf('django.db.models.fields.CharField')(max_length=64)), + )) + db.send_create_signal(u'default', ['Association']) + + # Adding model 'Code' + db.create_table('social_auth_code', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), + ('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), + ('verified', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'default', ['Code']) + + # Adding unique constraint on 'Code', fields ['email', 'code'] + db.create_unique('social_auth_code', ['email', 'code']) + + + def backwards(self, orm): + # Removing unique constraint on 'Code', fields ['email', 'code'] + db.delete_unique('social_auth_code', ['email', 'code']) + + # Removing unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + db.delete_unique('social_auth_usersocialauth', ['provider', 'uid']) + + # Deleting model 'UserSocialAuth' + db.delete_table('social_auth_usersocialauth') + + # Deleting model 'Nonce' + db.delete_table('social_auth_nonce') + + # Deleting model 'Association' + db.delete_table('social_auth_association') + + # Deleting model 'Code' + db.delete_table('social_auth_code') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'default.association': { + 'Meta': {'object_name': 'Association', 'db_table': "'social_auth_association'"}, + 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issued': ('django.db.models.fields.IntegerField', [], {}), + 'lifetime': ('django.db.models.fields.IntegerField', [], {}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'default.code': { + 'Meta': {'unique_together': "(('email', 'code'),)", 'object_name': 'Code', 'db_table': "'social_auth_code'"}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + u'default.nonce': { + 'Meta': {'object_name': 'Nonce', 'db_table': "'social_auth_nonce'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'salt': ('django.db.models.fields.CharField', [], {'max_length': '65'}), + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'timestamp': ('django.db.models.fields.IntegerField', [], {}) + }, + u'default.usersocialauth': { + 'Meta': {'unique_together': "(('provider', 'uid'),)", 'object_name': 'UserSocialAuth', 'db_table': "'social_auth_usersocialauth'"}, + 'extra_data': ('social.apps.django_app.default.fields.JSONField', [], {'default': "'{}'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'uid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['default'] \ No newline at end of file From e419e695b39a2948142faee3986d5cabe66e2cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 8 Jul 2014 13:56:36 -0300 Subject: [PATCH 290/890] Fix FK field descriptor for admin queries. Closes #322 --- social/apps/django_app/default/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index e1b787e69..9b6192d14 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -27,7 +27,7 @@ def get_search_fields(self, request=None): all_names = _User._meta.get_all_field_names() search_fields = [name for name in fieldnames if name and name in all_names] - return ['user_' + name for name in search_fields] + return ['user__' + name for name in search_fields] class NonceOption(admin.ModelAdmin): From 107b9355ed10104cb8b1b8010beaed04046b7286 Mon Sep 17 00:00:00 2001 From: David Grant Date: Tue, 15 Jul 2014 15:19:05 -0700 Subject: [PATCH 291/890] Minor typo. --- docs/backends/implementation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/implementation.rst b/docs/backends/implementation.rst index 2561dd2f6..ef2010c30 100644 --- a/docs/backends/implementation.rst +++ b/docs/backends/implementation.rst @@ -82,7 +82,7 @@ Shared attributes OAuth2 ****** -OAuth2 backends are fair simple to implement, just a few settings, a method +OAuth2 backends are fairly simple to implement; just a few settings, a method override and it's mostly ready to go. The key points on this backends are: From 325f5c06b314347cb92c1c6ab4a96afda879d59f Mon Sep 17 00:00:00 2001 From: David Grant Date: Tue, 15 Jul 2014 16:20:15 -0700 Subject: [PATCH 292/890] Slight retouch to spelling and wordage. --- docs/backends/implementation.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/backends/implementation.rst b/docs/backends/implementation.rst index ef2010c30..8687b9446 100644 --- a/docs/backends/implementation.rst +++ b/docs/backends/implementation.rst @@ -95,7 +95,7 @@ The key points on this backends are: ``ACCESS_TOKEN_URL`` Must point to the API endpoint that provides an ``access_token`` needed to - authenticate in users behalf on futer API calls. + authenticate in users behalf on future API calls. ``REFRESH_TOKEN_URL`` Some providers give the option to renew the ``access_token`` since they are @@ -110,12 +110,12 @@ The key points on this backends are: ``STATE_PARAMETER`` OAuth2 defines that an ``state`` parameter can be passed in order to - validate the process, it's kinda a CSRF check to avoid man in the middle - attacks. Some don't recognice it or don't return it which will making the - auth process invalid, set this attribute as ``False`` in such case. + validate the process, it's kind of a CSRF check to avoid man in the middle + attacks. Some don't recognise it or don't return it which will make the + auth process invalid. Set this attribute to ``False`` in that case. ``REDIRECT_STATE`` - For those providers that don't recognice the ``state`` parameter, the app + For those providers that don't recognise the ``state`` parameter, the app can add a ``redirect_state`` argument to the ``redirect_uri`` to mimic it. Set this value to ``False`` if the provider likes to verify the ``redirect_uri`` value and this parameter invalidates that check. From a9112f58e6e0a88740e5f252e6fee92efa5900ce Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Wed, 16 Jul 2014 04:20:48 +0100 Subject: [PATCH 293/890] Fixed #327 -- Changed access token method on backend. --- social/backends/github.py | 1 + social/tests/actions/actions.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/github.py b/social/backends/github.py index b41c0a4a4..18fdc678b 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -13,6 +13,7 @@ class GithubOAuth2(BaseOAuth2): name = 'github' AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize' ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' + ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' EXTRA_DATA = [ ('id', 'id'), diff --git a/social/tests/actions/actions.py b/social/tests/actions/actions.py index 14a8f6495..9d81dc83b 100644 --- a/social/tests/actions/actions.py +++ b/social/tests/actions/actions.py @@ -110,7 +110,7 @@ def do_login(self, after_complete_checks=True, user_data_body=None, expect(response.url).to.equal(location_url) expect(response.text).to.equal('foobar') - HTTPretty.register_uri(HTTPretty.GET, + HTTPretty.register_uri(HTTPretty.POST, uri=self.backend.ACCESS_TOKEN_URL, status=200, body=self.access_token_body or '', From 13fa6c2dc1483b20c3ae574cfd9640ad563f6aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 19 Jul 2014 18:10:05 -0300 Subject: [PATCH 294/890] Github for teams backend. Refs #329 --- docs/backends/github.rst | 17 ++++++++++ social/backends/github.py | 51 +++++++++++++++++----------- social/tests/backends/test_github.py | 41 ++++++++++++++++++++++ 3 files changed, 90 insertions(+), 19 deletions(-) diff --git a/docs/backends/github.rst b/docs/backends/github.rst index 188064f86..57a86bd90 100644 --- a/docs/backends/github.rst +++ b/docs/backends/github.rst @@ -33,4 +33,21 @@ Be sure to define the organization name using the setting:: This name will be used to check that the user really belongs to the given organization and discard it in case he's not part of it. + +Github for Teams +---------------- + +Similar to ``Github for Organizations``, there's a Github for Teams backend, +use the backend ``GithubTeamOAuth2``. The settings are the same than +the basic backend, but the names must be:: + + SOCIAL_AUTH_GITHUB_TEAM_* + +Be sure to define the ``Team Id`` using the setting:: + + SOCIAL_AUTH_GITHUB_TEAM_ID = '' + +This ``id`` will be used to check that the user really belongs to the given +team and discard it in case he's not part of it. + .. _GitHub Developers: https://github.com/settings/applications/new diff --git a/social/backends/github.py b/social/backends/github.py index 18fdc678b..30f98122e 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -50,31 +50,18 @@ def _user_data(self, access_token, path=None): return self.get_json(url, params={'access_token': access_token}) -class GithubOrganizationOAuth2(GithubOAuth2): - """Github OAuth2 authentication backend for organizations""" - name = 'github-org' - - def get_user_details(self, response): - """Return user details from Github account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - return {'username': response.get('login'), - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} +class GithubMemberOAuth2(GithubOAuth2): + no_member_string = '' def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - user_data = super(GithubOrganizationOAuth2, self).user_data( + user_data = super(GithubMemberOAuth2, self).user_data( access_token, *args, **kwargs ) - url = 'https://api.github.com/orgs/{org}/members/{username}'\ - .format(org=self.setting('NAME'), - username=user_data.get('login')) try: - self.request(url, params={'access_token': access_token}) + self.request(self.member_url(user_data), params={ + 'access_token': access_token + }) except HTTPError as err: # if the user is a member of the organization, response code # will be 204, see http://bit.ly/ZS6vFl @@ -82,3 +69,29 @@ def user_data(self, access_token, *args, **kwargs): raise AuthFailed(self, 'User doesn\'t belong to the organization') return user_data + + def member_url(self, user_data): + raise NotImplementedError('Implement in subclass') + + +class GithubOrganizationOAuth2(GithubMemberOAuth2): + """Github OAuth2 authentication backend for organizations""" + name = 'github-org' + no_member_string = 'User doesn\'t belong to the organization' + + def member_url(self, user_data): + return 'https://api.github.com/orgs/{org}/members/{username}'\ + .format(org=self.setting('NAME'), + username=user_data.get('login')) + + + +class GithubTeamOAuth2(GithubMemberOAuth2): + """Github OAuth2 authentication backend for teams""" + name = 'github-team' + no_member_string = 'User doesn\'t belong to the team' + + def member_url(self, user_data): + return 'https://api.github.com/teams/{team_id}/members/{username}'\ + .format(team_id=self.setting('ID'), + username=user_data.get('login')) diff --git a/social/tests/backends/test_github.py b/social/tests/backends/test_github.py index ab4f55652..e09001013 100644 --- a/social/tests/backends/test_github.py +++ b/social/tests/backends/test_github.py @@ -145,3 +145,44 @@ def test_login(self): def test_partial_pipeline(self): self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ORG_NAME': 'foobar'}) self.do_partial_pipeline.when.called_with().should.throw(AuthFailed) + + + +class GithubTeamOAuth2Test(GithubOAuth2Test): + backend_path = 'social.backends.github.GithubTeamOAuth2' + + def auth_handlers(self, start_url): + url = 'https://api.github.com/teams/123/members/foobar' + HTTPretty.register_uri(HTTPretty.GET, url, status=204, body='') + return super(GithubTeamOAuth2Test, self).auth_handlers( + start_url + ) + + def test_login(self): + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_TEAM_ID': '123'}) + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_TEAM_ID': '123'}) + self.do_partial_pipeline() + + +class GithubTeamOAuth2FailTest(GithubOAuth2Test): + backend_path = 'social.backends.github.GithubTeamOAuth2' + + def auth_handlers(self, start_url): + url = 'https://api.github.com/teams/123/members/foobar' + HTTPretty.register_uri(HTTPretty.GET, url, status=404, + body='{"message": "Not Found"}', + content_type='application/json') + return super(GithubTeamOAuth2FailTest, self).auth_handlers( + start_url + ) + + def test_login(self): + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_TEAM_ID': '123'}) + self.do_login.when.called_with().should.throw(AuthFailed) + + def test_partial_pipeline(self): + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_TEAM_ID': '123'}) + self.do_partial_pipeline.when.called_with().should.throw(AuthFailed) From fd0e1e710159b773dd9c20b32d6fef6118066f7f Mon Sep 17 00:00:00 2001 From: Mike Anderson Date: Tue, 22 Jul 2014 20:35:45 -0700 Subject: [PATCH 295/890] Don't overwrite clean_kwargs with kwargs --- social/pipeline/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/social/pipeline/utils.py b/social/pipeline/utils.py index eae1c3a1c..e296c3d5c 100644 --- a/social/pipeline/utils.py +++ b/social/pipeline/utils.py @@ -22,7 +22,6 @@ def partial_to_session(strategy, next, backend, request=None, *args, **kwargs): 'uid': social.uid } or None } - clean_kwargs.update(kwargs) # Clean any MergeDict data type from the values kwargs = {} From faa73164c23f458c20f10ac63ce75fcd0934e220 Mon Sep 17 00:00:00 2001 From: Mike Anderson Date: Wed, 23 Jul 2014 13:22:49 -0700 Subject: [PATCH 296/890] tests for two failing cases, include all kwargs in partial pipeline session --- social/pipeline/utils.py | 10 ++++--- social/tests/pipeline.py | 16 +++++++++++ social/tests/test_pipeline.py | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/social/pipeline/utils.py b/social/pipeline/utils.py index e296c3d5c..76b8e5d3a 100644 --- a/social/pipeline/utils.py +++ b/social/pipeline/utils.py @@ -22,23 +22,25 @@ def partial_to_session(strategy, next, backend, request=None, *args, **kwargs): 'uid': social.uid } or None } + + kwargs.update(clean_kwargs) # Clean any MergeDict data type from the values - kwargs = {} - for name, value in clean_kwargs.items(): + new_kwargs = {} + for name, value in kwargs.items(): # Check for class name to avoid importing Django MergeDict or # Werkzeug MultiDict if isinstance(value, dict) or \ value.__class__.__name__ in ('MergeDict', 'MultiDict'): value = dict(value) if isinstance(value, SERIALIZABLE_TYPES): - kwargs[name] = strategy.to_session_value(value) + new_kwargs[name] = strategy.to_session_value(value) return { 'next': next, 'backend': backend.name, 'args': tuple(map(strategy.to_session_value, args)), - 'kwargs': kwargs + 'kwargs': new_kwargs } diff --git a/social/tests/pipeline.py b/social/tests/pipeline.py index f12cb2b1c..b59de343a 100644 --- a/social/tests/pipeline.py +++ b/social/tests/pipeline.py @@ -26,3 +26,19 @@ def set_slug(strategy, user, *args, **kwargs): def remove_user(strategy, user, *args, **kwargs): return {'user': None} + +@partial +def set_user_from_kwargs(strategy, *args, **kwargs): + # from nose.tools import set_trace; set_trace() + if strategy.session_get('attribute'): + kwargs['user'].id + else: + return strategy.redirect(strategy.build_absolute_uri('/attribute')) + +@partial +def set_user_from_args(strategy, user, *args, **kwargs): + # from nose.tools import set_trace; set_trace() + if strategy.session_get('attribute'): + user.id + else: + return strategy.redirect(strategy.build_absolute_uri('/attribute')) diff --git a/social/tests/test_pipeline.py b/social/tests/test_pipeline.py index fd7778320..c2c2c4710 100644 --- a/social/tests/test_pipeline.py +++ b/social/tests/test_pipeline.py @@ -188,3 +188,55 @@ def test_multiple_accounts_with_same_email(self): user2.email = 'foo@bar.com' self.do_login.when.called_with(after_complete_checks=False)\ .should.throw(AuthException) + +class UserPersistsInPartialPipeline(BaseActionTest): + def test_user_persists_in_partial_pipeline_kwargs(self): + user = User(username='foobar1') + user.email = 'foo@bar.com' + + self.strategy.set_settings({ + 'SOCIAL_AUTH_PIPELINE': ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.associate_by_email', + 'social.tests.pipeline.set_user_from_kwargs' + ) + }) + + redirect = self.do_login(after_complete_checks=False) + + # Handle the partial pipeline + self.strategy.session_set('attribute', 'testing') + + data = self.strategy.session_pop('partial_pipeline') + + idx, backend, xargs, xkwargs = self.strategy.partial_from_session(data) + + self.backend.continue_pipeline(pipeline_index=idx, + *xargs, **xkwargs) + + + def test_user_persists_in_partial_pipeline(self): + user = User(username='foobar1') + user.email = 'foo@bar.com' + + self.strategy.set_settings({ + 'SOCIAL_AUTH_PIPELINE': ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.associate_by_email', + 'social.tests.pipeline.set_user_from_args' + ) + }) + + redirect = self.do_login(after_complete_checks=False) + + # Handle the partial pipeline + self.strategy.session_set('attribute', 'testing') + + data = self.strategy.session_pop('partial_pipeline') + + idx, backend, xargs, xkwargs = self.strategy.partial_from_session(data) + + self.backend.continue_pipeline(pipeline_index=idx, + *xargs, **xkwargs) From 9fd19214f456a064f298ae988e0e62dd1d803afd Mon Sep 17 00:00:00 2001 From: Mike Anderson Date: Wed, 23 Jul 2014 13:24:04 -0700 Subject: [PATCH 297/890] remove debugger --- social/tests/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/social/tests/pipeline.py b/social/tests/pipeline.py index b59de343a..133fc3c77 100644 --- a/social/tests/pipeline.py +++ b/social/tests/pipeline.py @@ -29,7 +29,6 @@ def remove_user(strategy, user, *args, **kwargs): @partial def set_user_from_kwargs(strategy, *args, **kwargs): - # from nose.tools import set_trace; set_trace() if strategy.session_get('attribute'): kwargs['user'].id else: @@ -37,7 +36,6 @@ def set_user_from_kwargs(strategy, *args, **kwargs): @partial def set_user_from_args(strategy, user, *args, **kwargs): - # from nose.tools import set_trace; set_trace() if strategy.session_get('attribute'): user.id else: From f1b4ebc59ecf5408afedb5ade77e1402f1eff7ad Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Thu, 24 Jul 2014 00:27:40 -0400 Subject: [PATCH 298/890] Support South and Django 1.7+ migrations. --- setup.py | 2 +- .../default/migrations/0001_initial.py | 371 +++++++++++------- 2 files changed, 228 insertions(+), 145 deletions(-) diff --git a/setup.py b/setup.py index faccfcbe7..2b5aabb6e 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'South>=0.8.4'] +requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0'] if PY3: requires += ['python3-openid>=3.0.1', 'requests-oauthlib>=0.3.0,<0.3.2'] diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 4ae51a176..082b81a60 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -1,149 +1,232 @@ # -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration +import django from django.db import models - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'UserSocialAuth' - db.create_table('social_auth_usersocialauth', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm['auth.User'])), - ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), - )) - db.send_create_signal(u'default', ['UserSocialAuth']) - - # Adding unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] - db.create_unique('social_auth_usersocialauth', ['provider', 'uid']) - - # Adding model 'Nonce' - db.create_table('social_auth_nonce', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('timestamp', self.gf('django.db.models.fields.IntegerField')()), - ('salt', self.gf('django.db.models.fields.CharField')(max_length=65)), - )) - db.send_create_signal(u'default', ['Nonce']) - - # Adding model 'Association' - db.create_table('social_auth_association', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('handle', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('issued', self.gf('django.db.models.fields.IntegerField')()), - ('lifetime', self.gf('django.db.models.fields.IntegerField')()), - ('assoc_type', self.gf('django.db.models.fields.CharField')(max_length=64)), - )) - db.send_create_signal(u'default', ['Association']) - - # Adding model 'Code' - db.create_table('social_auth_code', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), - ('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), - ('verified', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal(u'default', ['Code']) - - # Adding unique constraint on 'Code', fields ['email', 'code'] - db.create_unique('social_auth_code', ['email', 'code']) - - - def backwards(self, orm): - # Removing unique constraint on 'Code', fields ['email', 'code'] - db.delete_unique('social_auth_code', ['email', 'code']) - - # Removing unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] - db.delete_unique('social_auth_usersocialauth', ['provider', 'uid']) - - # Deleting model 'UserSocialAuth' - db.delete_table('social_auth_usersocialauth') - - # Deleting model 'Nonce' - db.delete_table('social_auth_nonce') - - # Deleting model 'Association' - db.delete_table('social_auth_association') - - # Deleting model 'Code' - db.delete_table('social_auth_code') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'default.association': { - 'Meta': {'object_name': 'Association', 'db_table': "'social_auth_association'"}, - 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'issued': ('django.db.models.fields.IntegerField', [], {}), - 'lifetime': ('django.db.models.fields.IntegerField', [], {}), - 'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - u'default.code': { - 'Meta': {'unique_together': "(('email', 'code'),)", 'object_name': 'Code', 'db_table': "'social_auth_code'"}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) - }, - u'default.nonce': { - 'Meta': {'object_name': 'Nonce', 'db_table': "'social_auth_nonce'"}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'salt': ('django.db.models.fields.CharField', [], {'max_length': '65'}), - 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'timestamp': ('django.db.models.fields.IntegerField', [], {}) - }, - u'default.usersocialauth': { - 'Meta': {'unique_together': "(('provider', 'uid'),)", 'object_name': 'UserSocialAuth', 'db_table': "'social_auth_usersocialauth'"}, - 'extra_data': ('social.apps.django_app.default.fields.JSONField', [], {'default': "'{}'"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'provider': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'uid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) +if django.get_version() < (1,7): + from south.utils import datetime_utils as datetime + from south.db import db + from south.v2 import SchemaMigration + + class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'UserSocialAuth' + db.create_table('social_auth_usersocialauth', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm['auth.User'])), + ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), + )) + db.send_create_signal(u'default', ['UserSocialAuth']) + + # Adding unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + db.create_unique('social_auth_usersocialauth', ['provider', 'uid']) + + # Adding model 'Nonce' + db.create_table('social_auth_nonce', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('timestamp', self.gf('django.db.models.fields.IntegerField')()), + ('salt', self.gf('django.db.models.fields.CharField')(max_length=65)), + )) + db.send_create_signal(u'default', ['Nonce']) + + # Adding model 'Association' + db.create_table('social_auth_association', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('handle', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('issued', self.gf('django.db.models.fields.IntegerField')()), + ('lifetime', self.gf('django.db.models.fields.IntegerField')()), + ('assoc_type', self.gf('django.db.models.fields.CharField')(max_length=64)), + )) + db.send_create_signal(u'default', ['Association']) + + # Adding model 'Code' + db.create_table('social_auth_code', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), + ('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), + ('verified', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'default', ['Code']) + + # Adding unique constraint on 'Code', fields ['email', 'code'] + db.create_unique('social_auth_code', ['email', 'code']) + + + def backwards(self, orm): + # Removing unique constraint on 'Code', fields ['email', 'code'] + db.delete_unique('social_auth_code', ['email', 'code']) + + # Removing unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + db.delete_unique('social_auth_usersocialauth', ['provider', 'uid']) + + # Deleting model 'UserSocialAuth' + db.delete_table('social_auth_usersocialauth') + + # Deleting model 'Nonce' + db.delete_table('social_auth_nonce') + + # Deleting model 'Association' + db.delete_table('social_auth_association') + + # Deleting model 'Code' + db.delete_table('social_auth_code') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'default.association': { + 'Meta': {'object_name': 'Association', 'db_table': "'social_auth_association'"}, + 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issued': ('django.db.models.fields.IntegerField', [], {}), + 'lifetime': ('django.db.models.fields.IntegerField', [], {}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'default.code': { + 'Meta': {'unique_together': "(('email', 'code'),)", 'object_name': 'Code', 'db_table': "'social_auth_code'"}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + u'default.nonce': { + 'Meta': {'object_name': 'Nonce', 'db_table': "'social_auth_nonce'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'salt': ('django.db.models.fields.CharField', [], {'max_length': '65'}), + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'timestamp': ('django.db.models.fields.IntegerField', [], {}) + }, + u'default.usersocialauth': { + 'Meta': {'unique_together': "(('provider', 'uid'),)", 'object_name': 'UserSocialAuth', 'db_table': "'social_auth_usersocialauth'"}, + 'extra_data': ('social.apps.django_app.default.fields.JSONField', [], {'default': "'{}'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'uid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) + } } - } - complete_apps = ['default'] \ No newline at end of file + complete_apps = ['default'] +else: + from __future__ import unicode_literals + + from django.db import models, migrations + import social.apps.django_app.default.fields + from django.conf import settings + import social.storage.django_orm + + + class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Association', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('server_url', models.CharField(max_length=255)), + ('handle', models.CharField(max_length=255)), + ('secret', models.CharField(max_length=255)), + ('issued', models.IntegerField()), + ('lifetime', models.IntegerField()), + ('assoc_type', models.CharField(max_length=64)), + ], + options={ + 'db_table': 'social_auth_association', + }, + bases=(models.Model, social.storage.django_orm.DjangoAssociationMixin), + ), + migrations.CreateModel( + name='Code', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('email', models.EmailField(max_length=75)), + ('code', models.CharField(db_index=True, max_length=32)), + ('verified', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'social_auth_code', + }, + bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), + ), + migrations.AlterUniqueTogether( + name='code', + unique_together=set([('email', 'code')]), + ), + migrations.CreateModel( + name='Nonce', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('server_url', models.CharField(max_length=255)), + ('timestamp', models.IntegerField()), + ('salt', models.CharField(max_length=65)), + ], + options={ + 'db_table': 'social_auth_nonce', + }, + bases=(models.Model, social.storage.django_orm.DjangoNonceMixin), + ), + migrations.CreateModel( + name='UserSocialAuth', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('provider', models.CharField(max_length=32)), + ('uid', models.CharField(max_length=255)), + ('extra_data', social.apps.django_app.default.fields.JSONField(default='{}')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'social_auth_usersocialauth', + }, + bases=(models.Model, social.storage.django_orm.DjangoUserMixin), + ), + migrations.AlterUniqueTogether( + name='usersocialauth', + unique_together=set([('provider', 'uid')]), + ), + ] From cb7fbdb55bd3c20bf07769b37b5cf22a796bb918 Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Thu, 24 Jul 2014 00:28:03 -0400 Subject: [PATCH 299/890] List test requirements. --- test_requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 test_requirements.txt diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 000000000..1d177113d --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,4 @@ +mock==1.0.1 +freeze==0.8.0 +sure==1.2.7 +httpretty==0.8.3 From b107dd2618cf6234d407e96fbb9d0e33dede6115 Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Thu, 24 Jul 2014 18:49:21 -0400 Subject: [PATCH 300/890] Fix an import issue in the Django migrations. --- social/apps/django_app/default/migrations/0001_initial.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 082b81a60..20d0eb2eb 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals + import django from django.db import models + if django.get_version() < (1,7): from south.utils import datetime_utils as datetime from south.db import db @@ -150,9 +153,7 @@ def backwards(self, orm): complete_apps = ['default'] else: - from __future__ import unicode_literals - - from django.db import models, migrations + from django.db import migrations import social.apps.django_app.default.fields from django.conf import settings import social.storage.django_orm From cdc4df32913949bdd2710bacae8dcba61869b794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 27 Jul 2014 15:15:44 -0300 Subject: [PATCH 301/890] Docs about writing custom pipeline functions --- docs/pipeline.rst | 119 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 5bba174a2..ee61806a3 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -178,5 +178,124 @@ defined:: Or individually by defining the setting per backend basis like ``SOCIAL_AUTH_TWITTER_FORCE_EMAIL_VALIDATION = True``. + +Extending the Pipeline +====================== + +The main purpose of the pipeline (either creation or deletion pipelines), is to +allow extensibility for developers, you can jump in the middle of it, do +changes to the data, create other models instances, ask users for data, or even +halt the whole process. + +Extending the pipeline implies: + + 1. Writing a function + 2. Locate it in a accessible path (accessible in the way that it can be + imported) + 3. Override the default pipeline definition with one that includes your + function. + +Writing the function is quite simple. Depending on the place you locate it will +determine the arguments it will receive, for example, adding your function +after ``social.pipeline.user.create_user`` ensures that you get the user +instance (created or already existent) instead of a ``None`` value. + +The pipeline functions will get quite a lot of arguments, ranging from the +backend in use, different model instances, server requests and provider +responses. To enumerate a few: + +``strategy`` + The current strategy instance. + +``backend`` + The current backend instance. + +``uid`` + User ID in the provider, this ``uid`` should identify the user in the + current provider. + +``response = {} or object()`` + The server user-details response, it depends on the protocol in use (and + sometimes the provider implementation of such protocol), but usually it's + just a ``dict`` with the user profile details in such provider. Lots of + information related to the user is provider here, sometimes the ``scope`` + will increase the amount of information in this response on OAuth + providers. + +``details = {}`` + Basic user details generated by the backend, used to create/update the user + model details (this ``dict`` will contain values like ``username``, + ``email``, ``first_name``, ``last_name`` and ``fullname``). + +``user = None`` + The user instance (or ``None`` if it wasn't created or retrieved from the + database yet). + +``social = None`` + This is the associated ``UserSocialAuth`` instance for the given user (or + ``None`` if it wasn't created or retrieved from the DB yet). + +Usually when writing your custom pipeline function, you just want to get some +values from the ``response`` parameter. But you can do even more, like call +other APIs endpoints to retrieve even more details about the user, store them +on some other place, etc. + +Here's an example of a simple pipeline function that will create a ``Profile`` +class related to the current user, this profile will store some simple details +returned by the provider (``Facebook`` in this example). The usual Facebook +``response`` looks like this:: + + { + 'username': 'foobar', + 'access_token': 'CAAD...', + 'first_name': 'Foo', + 'last_name': 'Bar', + 'verified': True, + 'name': 'Foo Bar', + 'locale': 'en_US', + 'gender': 'male', + 'expires': '5183999', + 'email': 'foo@bar.com', + 'updated_time': '2014-01-14T15:58:35+0000', + 'link': 'https://www.facebook.com/foobar', + 'timezone': -3, + 'id': '100000126636010' + } + +Let's say we are interested in storing the user profile link, the gender and +the timezone in our ``Profile`` model:: + + def save_profile(backend, user, response, *args, **kwargs): + if backend.name == 'facebook': + profile = user.get_profile() + if profile is None: + profile = Profile(user_id=user.id) + profile.gender = response.get('gender') + profile.link = response.get('link') + profile.timezone = response.get('timezone') + profile.save() + +Now all that's needed is to tell ``python-social-auth`` to use this function in +the pipeline, since it needs the user instance, it needs to be put after +``create_user`` function:: + + SOCIAL_AUTH_PIPELINE = ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.auth_allowed', + 'social.pipeline.social_auth.social_user', + 'social.pipeline.user.get_username', + 'social.pipeline.user.create_user', + 'import.path.to.save_profile', # <--- set the import-path to the function + 'social.pipeline.social_auth.associate_user', + 'social.pipeline.social_auth.load_extra_data', + 'social.pipeline.user.user_details' + ) + +If the return value of the function is a ``dict``, the values will be merged +into the next pipeline function parameters, so, for instance, if you want the +``profile`` instance to be available to the next function, all that it needs to +do is return ``{'profile': profile}``. + .. _python-social-auth: https://github.com/omab/python-social-auth .. _example applications: https://github.com/omab/python-social-auth/tree/master/examples From 34ee7386dacac6332fe4e296910f77235cd46ed4 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 28 Jul 2014 02:05:07 -0400 Subject: [PATCH 302/890] Clean up language in social/tests/README.rst These are just typos and grammatical fixes. --- social/tests/README.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/social/tests/README.rst b/social/tests/README.rst index f07546c21..fe94e5409 100644 --- a/social/tests/README.rst +++ b/social/tests/README.rst @@ -1,18 +1,18 @@ Testing python-social-auth ========================== -Testing the application is fair simple, just met the dependencies and run the -testing suite. +Testing the application is fairly simple. Just meet the dependencies and run +the testing suite. -The testing suite uses HTTPretty_ to mock server responses, it's not a live -test against the providers API, to do it that way, a browser and a tool like -Selenium are needed, that's slow, prone to errors on some cases, and some of -the application examples must be running to perform the testing. Plus real Key -and Secret pairs, in the end it's a mess to test functionality which is the -real point. +The testing suite uses HTTPretty_ to mock server responses. It's not a live +test against the provider's API. To do it that way, a browser and a tool like +Selenium are needed. That's slow, it's prone to errors in some cases, and some +of the application examples must be running to perform the testing. Plus, it +requires real Key and Secret pairs, in the end it's a mess to test +functionality which is the real point. -By mocking the server responses, we can test the backends functionality (and -other areas too) easily and quick. +By mocking the server responses, we can test the backend's functionality (and +other areas too) easily and quickly. Installing dependencies @@ -25,9 +25,9 @@ requirements.txt_. Then run with ``nosetests`` command. Pending ------- -At the moment only OAuth1 and OAuth2 backends are being tested, and just -login and partial pipeline features are covered by the test. There's still -a lot to work on, like: +At the moment only OAuth1 and OAuth2 backends are being tested, and only login +and partial pipeline features are covered by the test. There's still a lot to +work on, like: * OpenId backends * Frameworks support From 9db2f34c670b58c8f9836237df2757d63119465a Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Tue, 29 Jul 2014 10:32:06 +0100 Subject: [PATCH 303/890] Correct Stava scoping/permissions example. Signed-off-by: Chris Lamb --- docs/backends/strava.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/strava.rst b/docs/backends/strava.rst index 3623c830e..a2086c980 100644 --- a/docs/backends/strava.rst +++ b/docs/backends/strava.rst @@ -12,6 +12,6 @@ Strava uses OAuth v2 for Authentication. - extra scopes can be defined by using:: - SOCIAL_AUTH_INSTAGRAM_AUTH_EXTRA_ARGUMENTS = {'scope': 'likes comments relationships'} + SOCIAL_AUTH_STRAVA_SCOPE = ['view_private'] .. _Strava API: https://www.strava.com/settings/api From f6b9ee56928258661bc7c3289bc58c55b5bf3515 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Tue, 29 Jul 2014 10:35:16 +0100 Subject: [PATCH 304/890] Correct reference to 'firstname' when populating forenames from Strava. See: http://strava.github.io/api/v3/athlete/ Signed-off-by: Chris Lamb --- social/backends/strava.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/strava.py b/social/backends/strava.py index b59be7387..a70c57877 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -25,7 +25,7 @@ def get_user_details(self, response): username = response['athlete']['id'] email = response['athlete'].get('email', '') fullname, first_name, last_name = self.get_user_names( - first_name=response['athlete'].get('first_name', '') + first_name=response['athlete'].get('firstname', '') ) return {'username': str(username), 'fullname': fullname, From 5a2d00e0a1fc43abcd90a3dc647dc464b3ce4a24 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Tue, 29 Jul 2014 10:35:43 +0100 Subject: [PATCH 305/890] Also populate Strava name from 'lastname' attribute: Ref: http://strava.github.io/api/v3/athlete/ Signed-off-by: Chris Lamb --- social/backends/strava.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/strava.py b/social/backends/strava.py index a70c57877..b0a2a81f7 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -25,7 +25,8 @@ def get_user_details(self, response): username = response['athlete']['id'] email = response['athlete'].get('email', '') fullname, first_name, last_name = self.get_user_names( - first_name=response['athlete'].get('firstname', '') + first_name=response['athlete'].get('firstname', ''), + last_name=response['athlete'].get('lastname', ''), ) return {'username': str(username), 'fullname': fullname, From b9b0250073fd0515f19a9ba1a9c09d34b4951d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 2 Aug 2014 13:53:51 -0300 Subject: [PATCH 306/890] Enable DropboxOAuth2 on example app --- examples/django_example/example/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 62daabf7c..e4c9ca7ac 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -134,6 +134,7 @@ 'social.backends.disqus.DisqusOAuth2', 'social.backends.douban.DoubanOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.dropbox.DropboxOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.facebook.FacebookAppOAuth2', 'social.backends.facebook.FacebookOAuth2', From 5aed7c0a27c35e4f319bbc898457708310c4fecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 2 Aug 2014 13:57:38 -0300 Subject: [PATCH 307/890] Support redirect_state in OAuth1 backends too (enable twitter by default). Refs #338 --- social/backends/oauth.py | 125 +++++++++++++++++++-------------- social/backends/twitter.py | 1 + social/tests/backends/oauth.py | 26 ++++--- 3 files changed, 88 insertions(+), 64 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 3f091bf7a..21b3d201b 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -28,6 +28,8 @@ class OAuthAuth(BaseAuth): ACCESS_TOKEN_METHOD = 'GET' REVOKE_TOKEN_URL = None REVOKE_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + STATE_PARAMETER = False def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in @@ -36,6 +38,59 @@ def extra_data(self, user, uid, response, details=None): data['access_token'] = response.get('access_token', '') return data + def state_token(self): + """Generate csrf token to include as state parameter.""" + return self.strategy.random_string(32) + + def get_or_create_state(self): + if self.STATE_PARAMETER or self.REDIRECT_STATE: + # Store state in session for further request validation. The state + # value is passed as state parameter (as specified in OAuth2 spec), + # but also added to redirect, that way we can still verify the + # request if the provider doesn't implement the state parameter. + # Reuse token if any. + name = self.name + '_state' + state = self.strategy.session_get(name) + if state is None: + state = self.state_token() + self.strategy.session_set(name, state) + else: + state = None + return state + + def get_session_state(self): + return self.strategy.session_get(self.name + '_state') + + def get_request_state(self): + request_state = self.data.get('state') or \ + self.data.get('redirect_state') + if request_state and isinstance(request_state, list): + request_state = request_state[0] + return request_state + + def validate_state(self): + """Validate state value. Raises exception on error, returns state + value if valid.""" + if not self.STATE_PARAMETER and not self.REDIRECT_STATE: + return None + state = self.get_session_state() + request_state = self.get_request_state() + if not request_state: + raise AuthMissingParameter(self, 'state') + elif not state: + raise AuthStateMissing(self, 'state') + elif not request_state == state: + raise AuthStateForbidden(self) + else: + return state + + def get_redirect_uri(self, state=None): + """Build redirect with redirect_state parameter.""" + uri = self.redirect_uri + if self.REDIRECT_STATE and state: + uri = url_add_parameters(uri, {'redirect_state': state}) + return uri + def get_scope(self): """Return list with needed access scope""" scope = self.setting('SCOPE', []) @@ -109,6 +164,7 @@ def auth_complete(self, *args, **kwargs): """Return user, might be logged in""" # Multiple unauthorized tokens are supported (see #521) self.process_error(self.data) + self.validate_state() token = self.get_unauthorized_token() try: access_token = self.access_token(token) @@ -169,12 +225,14 @@ def unauthorized_token(self): # decoding='utf-8' produces errors with python-requests on Python3 # since the final URL will be of type bytes decoding = None if six.PY3 else 'utf-8' - response = self.request(self.REQUEST_TOKEN_URL, - params=params, - auth=OAuth1(key, secret, - callback_uri=self.redirect_uri, - decoding=decoding), - method=self.REQUEST_TOKEN_METHOD) + state = self.get_or_create_state() + response = self.request( + self.REQUEST_TOKEN_URL, + params=params, + auth=OAuth1(key, secret, callback_uri=self.get_redirect_uri(state), + decoding=decoding), + method=self.REQUEST_TOKEN_METHOD + ) content = response.content if response.encoding or response.apparent_encoding: content = content.decode(response.encoding or @@ -192,7 +250,8 @@ def oauth_authorization_request(self, token): params[self.OAUTH_TOKEN_PARAMETER_NAME] = token.get( self.OAUTH_TOKEN_PARAMETER_NAME ) - params[self.REDIRECT_URI_PARAMETER_NAME] = self.redirect_uri + state = self.get_or_create_state() + params[self.REDIRECT_URI_PARAMETER_NAME] = self.get_redirect_uri(state) return self.AUTHORIZATION_URL + '?' + urlencode(params) def oauth_auth(self, token=None, oauth_verifier=None, @@ -203,10 +262,11 @@ def oauth_auth(self, token=None, oauth_verifier=None, # decoding='utf-8' produces errors with python-requests on Python3 # since the final URL will be of type bytes decoding = None if six.PY3 else 'utf-8' + state = self.get_or_create_state() return OAuth1(key, secret, resource_owner_key=token.get('oauth_token'), resource_owner_secret=token.get('oauth_token_secret'), - callback_uri=self.redirect_uri, + callback_uri=self.get_redirect_uri(state), verifier=oauth_verifier, signature_type=signature_type, decoding=decoding) @@ -241,17 +301,6 @@ class BaseOAuth2(OAuthAuth): REDIRECT_STATE = True STATE_PARAMETER = True - def state_token(self): - """Generate csrf token to include as state parameter.""" - return self.strategy.random_string(32) - - def get_redirect_uri(self, state=None): - """Build redirect with redirect_state parameter.""" - uri = self.redirect_uri - if self.REDIRECT_STATE and state: - uri = url_add_parameters(uri, {'redirect_state': state}) - return uri - def auth_params(self, state=None): client_id, client_secret = self.get_key_and_secret() params = { @@ -266,20 +315,7 @@ def auth_params(self, state=None): def auth_url(self): """Return redirect url""" - if self.STATE_PARAMETER or self.REDIRECT_STATE: - # Store state in session for further request validation. The state - # value is passed as state parameter (as specified in OAuth2 spec), - # but also added to redirect, that way we can still verify the - # request if the provider doesn't implement the state parameter. - # Reuse token if any. - name = self.name + '_state' - state = self.strategy.session_get(name) - if state is None: - state = self.state_token() - self.strategy.session_set(name, state) - else: - state = None - + state = self.get_or_create_state() params = self.auth_params(state) params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) @@ -290,26 +326,6 @@ def auth_url(self): params = unquote(params) return self.AUTHORIZATION_URL + '?' + params - def validate_state(self): - """Validate state value. Raises exception on error, returns state - value if valid.""" - if not self.STATE_PARAMETER and not self.REDIRECT_STATE: - return None - state = self.strategy.session_get(self.name + '_state') - request_state = self.data.get('state') or \ - self.data.get('redirect_state') - if request_state and isinstance(request_state, list): - request_state = request_state[0] - - if not request_state: - raise AuthMissingParameter(self, 'state') - elif not state: - raise AuthStateMissing(self, 'state') - elif not request_state == state: - raise AuthStateForbidden(self) - else: - return state - def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { @@ -338,11 +354,12 @@ def process_error(self, data): def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" + state = self.validate_state() self.process_error(self.data) try: response = self.request_access_token( self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(self.validate_state()), + data=self.auth_complete_params(state), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD ) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index c096a3390..ed080eaf3 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -13,6 +13,7 @@ class TwitterOAuth(BaseOAuth1): AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authenticate' REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' + REDIRECT_STATE = True def process_error(self, data): if 'denied' in data: diff --git a/social/tests/backends/oauth.py b/social/tests/backends/oauth.py index 87961ac46..50a1b425d 100644 --- a/social/tests/backends/oauth.py +++ b/social/tests/backends/oauth.py @@ -4,7 +4,7 @@ from httpretty import HTTPretty from social.p3 import urlparse -from social.utils import parse_qs +from social.utils import parse_qs, url_add_parameters from social.tests.models import User from social.tests.backends.base import BaseBackendTest @@ -29,15 +29,21 @@ def _method(self, method): 'POST': HTTPretty.POST}[method] def handle_state(self, start_url, target_url): - try: - if self.backend.STATE_PARAMETER or self.backend.REDIRECT_STATE: - query = parse_qs(urlparse(start_url).query) - target_url = target_url + ('?' in target_url and '&' or '?') - if 'state' in query or 'redirect_state' in query: - name = 'state' in query and 'state' or 'redirect_state' - target_url += '{0}={1}'.format(name, query[name]) - except AttributeError: - pass + start_query = parse_qs(urlparse(start_url).query) + redirect_uri = start_query.get('redirect_uri') + + if getattr(self.backend, 'STATE_PARAMETER', False): + if start_query.get('state'): + target_url = url_add_parameters(target_url, { + 'state': start_query['state'] + }) + + if redirect_uri and getattr(self.backend, 'REDIRECT_STATE', False): + redirect_query = parse_qs(urlparse(redirect_uri).query) + if redirect_query.get('redirect_state'): + target_url = url_add_parameters(target_url, { + 'redirect_state': redirect_query['redirect_state'] + }) return target_url def auth_handlers(self, start_url): From 7c1cb547d3085929f99a58e790ab314800855ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 2 Aug 2014 14:06:39 -0300 Subject: [PATCH 308/890] Landscape conf --- .landscape.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .landscape.yaml diff --git a/.landscape.yaml b/.landscape.yaml new file mode 100644 index 000000000..c401c65e8 --- /dev/null +++ b/.landscape.yaml @@ -0,0 +1,5 @@ +doc-warnings: no +test-warnings: no +strictness: medium +max-line-length: 80 +autodetect: no From c389cac4839674d4b350eae34b2fa9b5b918c14b Mon Sep 17 00:00:00 2001 From: Vadym Petrychenko Date: Tue, 5 Aug 2014 11:32:13 +0200 Subject: [PATCH 309/890] Update vk.rst Update links to the VK documentation. --- docs/backends/vk.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backends/vk.rst b/docs/backends/vk.rst index 53ad328d1..ea9a2f373 100644 --- a/docs/backends/vk.rst +++ b/docs/backends/vk.rst @@ -125,7 +125,7 @@ Snippet example:: Click to authorize -.. _VK.com OAuth: http://vk.com/developers.php?oid=-1&p=%D0%90%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F_%D1%81%D0%B0%D0%B9%D1%82%D0%BE%D0%B2 -.. _VK.com list of permissions: http://vk.com/developers.php?oid=-1&p=%D0%9F%D1%80%D0%B0%D0%B2%D0%B0_%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%D0%B0_%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B9 -.. _VK.com API: http://vk.com/developers.php +.. _VK.com OAuth: http://vk.com/dev/authentication +.. _VK.com list of permissions: http://vk.com/dev/permissions +.. _VK.com API: http://vk.com/dev/methods .. _authentication for VK.com applications: http://www.ikrvss.ru/2011/11/08/django-social-auh-and-vkontakte-application/ From 47ed69bc4c3f78557e8646785e6024b7e28e1e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 7 Aug 2014 13:21:17 -0300 Subject: [PATCH 310/890] Simplify moves backend code and add documentation. Refs #307 --- docs/backends/index.rst | 1 + docs/backends/moves.rst | 31 +++++++++++++++++++++ examples/django_example/example/settings.py | 1 + social/backends/moves.py | 21 ++------------ 4 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 docs/backends/moves.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index fdf01ed49..cc95ca979 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -82,6 +82,7 @@ Social backends mapmyfitness mendeley mixcloud + moves odnoklassnikiru openstreetmap persona diff --git a/docs/backends/moves.rst b/docs/backends/moves.rst new file mode 100644 index 000000000..d5d7ad924 --- /dev/null +++ b/docs/backends/moves.rst @@ -0,0 +1,31 @@ +Moves +===== + +Moves_ provides an OAuth2 authentication flow. In order to enable it: + +- Register an application at `Manage Your Apps`_, remember to fill the + ``Redirect URI`` once the application was created. + +- Fill **Client ID** and **Client secret** in the settings:: + + SOCIAL_AUTH_MOVES_KEY = '' + SOCIAL_AUTH_MOVES_SECRET = '' + +- Define the mandatory scope for your application:: + + SOCIAL_AUTH_MOVES_SCOPE = ['activity', 'location'] + + The scope parameter is required by Moves_ but the backend doesn't set + a default one to minimize the application permissions request, so it's + mandatory for the developer to define this setting. + +- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.moves.MovesOAuth2', + ... + ) + +.. _Moves: http://moves-app.com/ +.. _Manage Your Apps: https://dev.moves-app.com/apps diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index e4c9ca7ac..9243408cf 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -192,6 +192,7 @@ 'social.backends.yandex.YandexOAuth2', 'social.backends.vimeo.VimeoOAuth1', 'social.backends.lastfm.LastFmAuth', + 'social.backends.moves.MovesOAuth2', 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', diff --git a/social/backends/moves.py b/social/backends/moves.py index 6f681e3b2..5464f7bed 100644 --- a/social/backends/moves.py +++ b/social/backends/moves.py @@ -11,16 +11,10 @@ class MovesOAuth2(BaseOAuth2): """Moves OAuth authentication backend""" name = 'moves' - - # From https://dev.moves-app.com/docs/authentication#authorization - AUTHORIZATION_URL = 'https://api.moves-app.com/oauth/v1/authorize' - ACCESS_TOKEN_URL = 'https://api.moves-app.com/oauth/v1/access_token?grant_type=authorization_code' - REFRESH_TOKEN_URL = 'https://api.moves-app.com/oauth/v1/access_token?grant_type=refresh_token' - ID_KEY = 'user_id' - REDIRECT_STATE = True + AUTHORIZATION_URL = 'https://api.moves-app.com/oauth/v1/authorize' + ACCESS_TOKEN_URL = 'https://api.moves-app.com/oauth/v1/access_token' ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ' ' EXTRA_DATA = [ ('refresh_token', 'refresh_token', True), ('expires_in', 'expires'), @@ -32,14 +26,5 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - params = self.setting('PROFILE_EXTRA_PARAMS', {}) - params['access_token'] = access_token return self.get_json('https://api.moves-app.com/api/1.1/user/profile', - params=params) - - def refresh_token(self, token, *args, **kwargs): - params = self.refresh_token_params(token, *args, **kwargs) - request = self.request(self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL, - data=params, headers=self.auth_headers(), - method='POST') - return self.process_refresh_token_response(request, *args, **kwargs) + params={'access_token': access_token}) From 83baa5fbe1c9e2b9df9c7711ae48b1da82b8101f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 7 Aug 2014 13:36:14 -0300 Subject: [PATCH 311/890] Fix user syncdb. Refs #342 --- examples/tornado_example/app.py | 3 ++- examples/tornado_example/models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/tornado_example/app.py b/examples/tornado_example/app.py index 71a178e7f..ad900ed98 100644 --- a/examples/tornado_example/app.py +++ b/examples/tornado_example/app.py @@ -59,9 +59,10 @@ def main(): def syncdb(): - from models import User + from models import user_syncdb init_social(Base, session, tornado_settings) Base.metadata.create_all(engine) + user_syncdb() if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == 'syncdb': diff --git a/examples/tornado_example/models.py b/examples/tornado_example/models.py index 784f2cf40..19b98a972 100644 --- a/examples/tornado_example/models.py +++ b/examples/tornado_example/models.py @@ -13,5 +13,5 @@ class User(Base): password = Column(String(128), nullable=True) -if __name__ == '__main__': +def user_syncdb(): Base.metadata.create_all(engine) From 2fc4e8cbab9c9ea196390ada2fd33ace82848b76 Mon Sep 17 00:00:00 2001 From: Josh Probst Date: Fri, 8 Aug 2014 11:51:56 -0600 Subject: [PATCH 312/890] numeric index for format django_app _do_login function needs to use numeric index when formatting string --- social/apps/django_app/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 6f23a42b0..bf326d13f 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -31,7 +31,7 @@ def disconnect(request, backend, association_id=None): def _do_login(backend, user, social_user): - user.backend = '{}.{}'.format(backend.__module__, + user.backend = '{0}.{1}'.format(backend.__module__, backend.__class__.__name__) login(backend.strategy.request, user) if backend.setting('SESSION_EXPIRATION', True): From 1084d53e0ece823b2c1d81e49f28177f627e5215 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Fri, 8 Aug 2014 20:20:37 -0400 Subject: [PATCH 313/890] Added Open ID Connect base backend See #300 --- requirements-python3.txt | 1 + requirements.txt | 1 + social/backends/google.py | 1 + social/backends/open_id.py | 119 ++++++++++++++++++++++++++- social/tests/backends/open_id.py | 93 +++++++++++++++++++++ social/tests/backends/test_google.py | 8 +- 6 files changed, 220 insertions(+), 3 deletions(-) diff --git a/requirements-python3.txt b/requirements-python3.txt index b8ca41b0a..bb7f4f532 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -3,3 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.0,<0.3.2 six>=1.2.0 +PyJWT==0.2.1 diff --git a/requirements.txt b/requirements.txt index fa8c222ef..960b9da5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.0 six>=1.2.0 +PyJWT==0.2.1 diff --git a/social/backends/google.py b/social/backends/google.py index 3913c19ed..ef5868e42 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -205,6 +205,7 @@ def get_user_id(self, details, response): class GoogleOpenIdConnect(GoogleOAuth2, OpenIdConnectAuth): name = 'google-openidconnect' + ID_TOKEN_ISSUER = "accounts.google.com" def user_data(self, access_token, *args, **kwargs): """Return user data from Google API""" diff --git a/social/backends/open_id.py b/social/backends/open_id.py index c966a8259..173db97d0 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -1,10 +1,13 @@ +from calendar import timegm +import datetime +import jwt from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE from openid.consumer.discover import DiscoveryFailure from openid.extensions import sreg, ax, pape from social.utils import url_add_parameters from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthMissingParameter + AuthUnknownError, AuthMissingParameter, AuthTokenError from social.backends.base import BaseAuth from social.backends.oauth import BaseOAuth2 @@ -252,6 +255,118 @@ def openid_url(self): raise AuthMissingParameter(self, OPENID_ID_FIELD) +class OpenIdConnectAssociation(object): + """ Use Association model to save the nonce by force. """ + + def __init__(self, handle, secret='', issued=0, lifetime=0, assoc_type=''): + self.handle = handle # as nonce + self.secret = secret.encode() # not use + self.issued = issued # not use + self.lifetime = lifetime # not use + self.assoc_type = assoc_type # as state + + class OpenIdConnectAuth(BaseOAuth2): + """ + Base class for Open ID Connect backends. + + Currently only the code response type is supported. + """ + + ID_TOKEN_ISSUER = None DEFAULT_SCOPE = ['openid'] - EXTRA_DATA = ['id_token', 'refresh_token'] + EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')] + + # Set after access_token is retrieved + id_token = None + + def auth_params(self, state=None): + """Return extra arguments needed on auth process.""" + params = super(OpenIdConnectAuth, self).auth_params(state) + + params['nonce'] = self._get_and_store_nonce(self.AUTHORIZATION_URL, state) + + return params + + def auth_complete_params(self, state=None): + params = super(OpenIdConnectAuth, self).auth_complete_params(state) + + # Add a nonce to the request so that to help counter CSRF + params['nonce'] = self._get_and_store_nonce(self.ACCESS_TOKEN_URL, state) + + return params + + def _get_and_store_nonce(self, url, state): + # Create a nonce + nonce = self.strategy.random_string(64) + + # Store the nonce + association = OpenIdConnectAssociation(nonce, assoc_type=state) + self.strategy.storage.association.store(url, association) + + return nonce + + def _get_nonce(self, nonce): + server_url = self.ACCESS_TOKEN_URL + try: + return self.strategy.storage.association.get(server_url=server_url, handle=nonce)[0] + except: # pylint: disable=bare-except + return None + + def _remove_nonce(self, nonce_id): + try: + self.strategy.storage.association.remove([nonce_id]) + except: # pylint: disable=bare-except + return None + + def _validate_and_return_id_token(self, id_token): + """ + Validates the id_token according to the steps at + http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. + """ + + client_id, _client_secret = self.get_key_and_secret() + + try: + # Decode the JWT and raise an error if the secret is invalid or + # the response has expired. + decryption_key = self.setting('ID_TOKEN_DECRYPTION_KEY') + id_token = jwt.decode(id_token, decryption_key) + except (jwt.DecodeError, jwt.ExpiredSignature) as de: + raise AuthTokenError(self, de) + + # Verify the issuer of the id_token is correct + if id_token['iss'] != self.ID_TOKEN_ISSUER: + raise AuthTokenError(self, 'Incorrect id_token: iss') + + # Verify the token was issued in the last 10 minutes + utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) + if id_token['iat'] < (utc_timestamp - 600): + raise AuthTokenError(self, 'Incorrect id_token: iat') + + # Verify this client is the correct recipient of the id_token + aud = id_token.get('aud') + if aud != client_id: + raise AuthTokenError(self, 'Incorrect id_token: aud') + + # Validate the nonce to ensure the request was not modified + nonce = id_token.get('nonce') + if not nonce: + raise AuthTokenError(self, 'Incorrect id_token: nonce') + + nonce_obj = self._get_nonce(id_token['nonce']) + if nonce_obj: + self._remove_nonce(nonce_obj.id) + else: + raise AuthTokenError(self, 'Incorrect id_token: nonce') + + return id_token + + def request_access_token(self, *args, **kwargs): + """ + Retrieve the access token. Also, validate the id_token and + store it (temporarily). + """ + response = self.get_json(*args, **kwargs) + self.id_token = self._validate_and_return_id_token(response['id_token']) + return response diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index 1af517d77..f39165f16 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -1,7 +1,12 @@ # -*- coding: utf-8 -*- +from calendar import timegm +import json import sys +import datetime +import jwt import requests from openid import oidutil +from social.exceptions import AuthTokenError PY3 = sys.version_info[0] == 3 @@ -109,3 +114,91 @@ def do_start(self): status=200, body='is_valid:true\n') return self.backend.complete() + + +class OpenIdConnectTestMixin(object): + """ + Mixin to test OpenID Connect consumers. Inheriting classes should also inherit OAuth2Test. + """ + + client_key = 'a-key' + client_secret = 'a-secret-key' + issuer = None # id_token issuer + + def setUp(self): + super(OpenIdConnectTestMixin, self).setUp() + self.access_token_body = self._parse_nonce_and_return_access_token_body + + def extra_settings(self): + xs = super(OpenIdConnectTestMixin, self).extra_settings() + xs.update({ + 'SOCIAL_AUTH_{}_KEY'.format(self.name): self.client_key, + 'SOCIAL_AUTH_{}_SECRET'.format(self.name): self.client_secret, + 'SOCIAL_AUTH_{}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): self.client_secret + }) + return xs + + def _parse_nonce_and_return_access_token_body(self, request, _url, headers): + """ + Get the nonce from the request parameters, add it to the id_token, and return the complete response. + """ + body = self.prepare_access_token_body(nonce=request.parsed_body[u'nonce'][0]) + return 200, headers, body + + def prepare_access_token_body(self, client_key=None, client_secret=None, expiration_datetime=None, + issue_datetime=None, nonce=None, issuer=None): + """ + Prepares a provider access token response + + Arguments + client_id (str) -- OAuth ID for the client that requested authentication. + client_secret (str) -- OAuth secret for the client that requested authentication. + expiration_time (datetime) -- Date and time after which the response should be considered invalid. + """ + + body = {'access_token': 'foobar', 'token_type': 'bearer'} + client_key = client_key or self.client_key + client_secret = client_secret or self.client_secret + now = datetime.datetime.utcnow() + expiration_datetime = expiration_datetime or (now + datetime.timedelta(seconds=30)) + issue_datetime = issue_datetime or now + nonce = nonce or None + issuer = issuer or self.issuer + + id_token = { + u'iss': issuer, + u'nonce': nonce, + u'aud': client_key, + u'azp': client_key, + u'exp': timegm(expiration_datetime.utctimetuple()), + u'iat': timegm(issue_datetime.utctimetuple()), + u'sub': u'1234', + } + + body[u'id_token'] = jwt.encode(id_token, client_secret) + + return json.dumps(body) + + def assertAutTokenErrorRaised(self, expected_message, **access_token_kwargs): + self.access_token_body = self.prepare_access_token_body(**access_token_kwargs) + self.assertRaisesRegexp(AuthTokenError, expected_message, self.do_login) + + def test_invalid_secret(self): + self.assertAutTokenErrorRaised('Token error: Signature verification failed', client_secret='wrong!') + + def test_expired_signature(self): + expiration_datetime = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) + self.assertAutTokenErrorRaised('Token error: Signature has expired', expiration_datetime=expiration_datetime) + + def test_invalid_issuer(self): + self.assertAutTokenErrorRaised('Token error: Incorrect id_token: iss', issuer='someone-else') + + def test_invalid_audience(self): + self.assertAutTokenErrorRaised('Token error: Incorrect id_token: aud', client_key='someone-else') + + def test_invalid_issue_time(self): + expiration_datetime = datetime.datetime.utcnow() - datetime.timedelta(hours=1) + self.assertAutTokenErrorRaised('Token error: Incorrect id_token: iat', issue_datetime=expiration_datetime) + + def test_invalid_nonce(self): + self.assertAutTokenErrorRaised('Token error: Incorrect id_token: nonce', nonce='something-wrong') diff --git a/social/tests/backends/test_google.py b/social/tests/backends/test_google.py index 4f1a40c66..47f0215ff 100644 --- a/social/tests/backends/test_google.py +++ b/social/tests/backends/test_google.py @@ -8,7 +8,7 @@ from social.tests.models import User from social.tests.backends.oauth import OAuth1Test, OAuth2Test -from social.tests.backends.open_id import OpenIdTest +from social.tests.backends.open_id import OpenIdTest, OpenIdConnectTestMixin class GoogleOAuth2Test(OAuth2Test): @@ -280,3 +280,9 @@ def test_revoke_token(self): self.backend.REVOKE_TOKEN_URL, status=200) do_disconnect(self.backend, user) + + +class GoogleOpenIdConnectTest(OpenIdConnectTestMixin, GoogleOAuth2Test): + backend_path = 'social.backends.google.GoogleOpenIdConnect' + user_data_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' + issuer = "accounts.google.com" From 0955d97db446d1f3929f3de856df08e7c6cf8593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 9 Aug 2014 18:00:55 -0300 Subject: [PATCH 314/890] PEP8 and fixed tests. Refs #348 --- setup.py | 6 +- social/backends/open_id.py | 63 +++++++------- social/tests/backends/open_id.py | 122 ++++++++++++++++----------- social/tests/backends/test_google.py | 111 ++++++++++++------------ social/tests/requirements.txt | 1 + 5 files changed, 156 insertions(+), 147 deletions(-) diff --git a/setup.py b/setup.py index 2b5aabb6e..c94788686 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def long_description(): def path_tokens(path): if not path: return [] - head, tail = os.path.split(path) + head, tail = split(path) return path_tokens(head) + [tail] @@ -46,7 +46,7 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0'] +requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT>=0.2.1'] if PY3: requires += ['python3-openid>=3.0.1', 'requests-oauthlib>=0.3.0,<0.3.2'] @@ -63,7 +63,7 @@ def get_packages(): keywords='django, flask, pyramid, webpy, openid, oauth, social auth', url='https://github.com/omab/python-social-auth', packages=get_packages(), - #package_data={'social': ['locale/*/LC_MESSAGES/*']}, + # package_data={'social': ['locale/*/LC_MESSAGES/*']}, long_description=long_description(), install_requires=requires, classifiers=['Development Status :: 4 - Beta', diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 173db97d0..0fd28f5db 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -1,13 +1,16 @@ -from calendar import timegm import datetime -import jwt +from calendar import timegm + +from jwt import DecodeError, ExpiredSignature, decode as jwt_decode + from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE from openid.consumer.discover import DiscoveryFailure from openid.extensions import sreg, ax, pape from social.utils import url_add_parameters from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthMissingParameter, AuthTokenError + AuthUnknownError, AuthMissingParameter, \ + AuthTokenError from social.backends.base import BaseAuth from social.backends.oauth import BaseOAuth2 @@ -272,67 +275,60 @@ class OpenIdConnectAuth(BaseOAuth2): Currently only the code response type is supported. """ - ID_TOKEN_ISSUER = None DEFAULT_SCOPE = ['openid'] EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')] - # Set after access_token is retrieved id_token = None def auth_params(self, state=None): """Return extra arguments needed on auth process.""" params = super(OpenIdConnectAuth, self).auth_params(state) - - params['nonce'] = self._get_and_store_nonce(self.AUTHORIZATION_URL, state) - + params['nonce'] = self.get_and_store_nonce( + self.AUTHORIZATION_URL, state + ) return params def auth_complete_params(self, state=None): params = super(OpenIdConnectAuth, self).auth_complete_params(state) - # Add a nonce to the request so that to help counter CSRF - params['nonce'] = self._get_and_store_nonce(self.ACCESS_TOKEN_URL, state) - + params['nonce'] = self.get_and_store_nonce( + self.ACCESS_TOKEN_URL, state + ) return params - def _get_and_store_nonce(self, url, state): + def get_and_store_nonce(self, url, state): # Create a nonce nonce = self.strategy.random_string(64) - # Store the nonce association = OpenIdConnectAssociation(nonce, assoc_type=state) self.strategy.storage.association.store(url, association) - return nonce - def _get_nonce(self, nonce): - server_url = self.ACCESS_TOKEN_URL + def get_nonce(self, nonce): try: - return self.strategy.storage.association.get(server_url=server_url, handle=nonce)[0] - except: # pylint: disable=bare-except - return None + return self.strategy.storage.association.get( + server_url=self.ACCESS_TOKEN_URL, + handle=nonce + )[0] + except IndexError: + pass - def _remove_nonce(self, nonce_id): - try: - self.strategy.storage.association.remove([nonce_id]) - except: # pylint: disable=bare-except - return None + def remove_nonce(self, nonce_id): + self.strategy.storage.association.remove([nonce_id]) - def _validate_and_return_id_token(self, id_token): + def validate_and_return_id_token(self, id_token): """ Validates the id_token according to the steps at http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. """ - client_id, _client_secret = self.get_key_and_secret() - + decryption_key = self.setting('ID_TOKEN_DECRYPTION_KEY') try: # Decode the JWT and raise an error if the secret is invalid or # the response has expired. - decryption_key = self.setting('ID_TOKEN_DECRYPTION_KEY') - id_token = jwt.decode(id_token, decryption_key) - except (jwt.DecodeError, jwt.ExpiredSignature) as de: + id_token = jwt_decode(id_token, decryption_key) + except (DecodeError, ExpiredSignature) as de: raise AuthTokenError(self, de) # Verify the issuer of the id_token is correct @@ -354,12 +350,11 @@ def _validate_and_return_id_token(self, id_token): if not nonce: raise AuthTokenError(self, 'Incorrect id_token: nonce') - nonce_obj = self._get_nonce(id_token['nonce']) + nonce_obj = self.get_nonce(nonce) if nonce_obj: - self._remove_nonce(nonce_obj.id) + self.remove_nonce(nonce_obj.id) else: raise AuthTokenError(self, 'Incorrect id_token: nonce') - return id_token def request_access_token(self, *args, **kwargs): @@ -368,5 +363,5 @@ def request_access_token(self, *args, **kwargs): store it (temporarily). """ response = self.get_json(*args, **kwargs) - self.id_token = self._validate_and_return_id_token(response['id_token']) + self.id_token = self.validate_and_return_id_token(response['id_token']) return response diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index f39165f16..3c608c748 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- from calendar import timegm -import json + import sys +import json import datetime -import jwt + import requests +import jwt + from openid import oidutil -from social.exceptions import AuthTokenError + PY3 = sys.version_info[0] == 3 @@ -22,7 +25,7 @@ from social.utils import parse_qs, module_member from social.backends.utils import load_backends - +from social.exceptions import AuthTokenError from social.tests.backends.base import BaseBackendTest from social.tests.models import TestStorage, User, TestUserSocialAuth, \ TestNonce, TestAssociation @@ -118,87 +121,104 @@ def do_start(self): class OpenIdConnectTestMixin(object): """ - Mixin to test OpenID Connect consumers. Inheriting classes should also inherit OAuth2Test. + Mixin to test OpenID Connect consumers. Inheriting classes should also + inherit OAuth2Test. """ - client_key = 'a-key' client_secret = 'a-secret-key' issuer = None # id_token issuer - def setUp(self): - super(OpenIdConnectTestMixin, self).setUp() - self.access_token_body = self._parse_nonce_and_return_access_token_body - def extra_settings(self): - xs = super(OpenIdConnectTestMixin, self).extra_settings() - xs.update({ - 'SOCIAL_AUTH_{}_KEY'.format(self.name): self.client_key, - 'SOCIAL_AUTH_{}_SECRET'.format(self.name): self.client_secret, - 'SOCIAL_AUTH_{}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): self.client_secret + settings = super(OpenIdConnectTestMixin, self).extra_settings() + settings.update({ + 'SOCIAL_AUTH_{0}_KEY'.format(self.name): self.client_key, + 'SOCIAL_AUTH_{0}_SECRET'.format(self.name): self.client_secret, + 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): + self.client_secret }) - return xs + return settings - def _parse_nonce_and_return_access_token_body(self, request, _url, headers): + def parse_nonce_and_return_access_token_body(self, request, _url, headers): """ - Get the nonce from the request parameters, add it to the id_token, and return the complete response. + Get the nonce from the request parameters, add it to the id_token, and + return the complete response. """ - body = self.prepare_access_token_body(nonce=request.parsed_body[u'nonce'][0]) + nonce = parse_qs(request.body).get('nonce') + body = self.prepare_access_token_body(nonce=nonce) return 200, headers, body - def prepare_access_token_body(self, client_key=None, client_secret=None, expiration_datetime=None, - issue_datetime=None, nonce=None, issuer=None): + def prepare_access_token_body(self, client_key=None, client_secret=None, + expiration_datetime=None, + issue_datetime=None, nonce=None, + issuer=None): """ - Prepares a provider access token response - - Arguments - client_id (str) -- OAuth ID for the client that requested authentication. - client_secret (str) -- OAuth secret for the client that requested authentication. - expiration_time (datetime) -- Date and time after which the response should be considered invalid. + Prepares a provider access token response. Arguments: + + client_id -- (str) OAuth ID for the client that requested + authentication. + client_secret -- (str) OAuth secret for the client that requested + authentication. + expiration_time -- (datetime) Date and time after which the response + should be considered invalid. """ body = {'access_token': 'foobar', 'token_type': 'bearer'} client_key = client_key or self.client_key client_secret = client_secret or self.client_secret now = datetime.datetime.utcnow() - expiration_datetime = expiration_datetime or (now + datetime.timedelta(seconds=30)) + expiration_datetime = expiration_datetime or \ + (now + datetime.timedelta(seconds=30)) issue_datetime = issue_datetime or now - nonce = nonce or None + nonce = nonce or 'a-nonce' issuer = issuer or self.issuer - id_token = { - u'iss': issuer, - u'nonce': nonce, - u'aud': client_key, - u'azp': client_key, - u'exp': timegm(expiration_datetime.utctimetuple()), - u'iat': timegm(issue_datetime.utctimetuple()), - u'sub': u'1234', + 'iss': issuer, + 'nonce': nonce, + 'aud': client_key, + 'azp': client_key, + 'exp': timegm(expiration_datetime.utctimetuple()), + 'iat': timegm(issue_datetime.utctimetuple()), + 'sub': '1234', } - - body[u'id_token'] = jwt.encode(id_token, client_secret) - + body['id_token'] = jwt.encode(id_token, client_secret).decode('utf-8') return json.dumps(body) - def assertAutTokenErrorRaised(self, expected_message, **access_token_kwargs): - self.access_token_body = self.prepare_access_token_body(**access_token_kwargs) - self.assertRaisesRegexp(AuthTokenError, expected_message, self.do_login) + def authtoken_raised(self, expected_message, **access_token_kwargs): + self.access_token_body = self.prepare_access_token_body( + **access_token_kwargs + ) + self.do_login.when.called_with().should.throw( + AuthTokenError, expected_message + ) def test_invalid_secret(self): - self.assertAutTokenErrorRaised('Token error: Signature verification failed', client_secret='wrong!') + self.authtoken_raised( + 'Token error: Signature verification failed', + client_secret='wrong!' + ) def test_expired_signature(self): - expiration_datetime = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) - self.assertAutTokenErrorRaised('Token error: Signature has expired', expiration_datetime=expiration_datetime) + expiration_datetime = datetime.datetime.utcnow() - \ + datetime.timedelta(seconds=30) + self.authtoken_raised('Token error: Signature has expired', + expiration_datetime=expiration_datetime) def test_invalid_issuer(self): - self.assertAutTokenErrorRaised('Token error: Incorrect id_token: iss', issuer='someone-else') + self.authtoken_raised('Token error: Incorrect id_token: iss', + issuer='someone-else') def test_invalid_audience(self): - self.assertAutTokenErrorRaised('Token error: Incorrect id_token: aud', client_key='someone-else') + self.authtoken_raised('Token error: Incorrect id_token: aud', + client_key='someone-else') def test_invalid_issue_time(self): - expiration_datetime = datetime.datetime.utcnow() - datetime.timedelta(hours=1) - self.assertAutTokenErrorRaised('Token error: Incorrect id_token: iat', issue_datetime=expiration_datetime) + expiration_datetime = datetime.datetime.utcnow() - \ + datetime.timedelta(hours=1) + self.authtoken_raised('Token error: Incorrect id_token: iat', + issue_datetime=expiration_datetime) def test_invalid_nonce(self): - self.assertAutTokenErrorRaised('Token error: Incorrect id_token: nonce', nonce='something-wrong') + self.authtoken_raised( + 'Token error: Incorrect id_token: nonce', + nonce='something-wrong' + ) diff --git a/social/tests/backends/test_google.py b/social/tests/backends/test_google.py index 47f0215ff..0f17b47d7 100644 --- a/social/tests/backends/test_google.py +++ b/social/tests/backends/test_google.py @@ -1,5 +1,5 @@ -import json import datetime +import json from httpretty import HTTPretty @@ -169,65 +169,53 @@ class GoogleOpenIdTest(OpenIdTest): backend_path = 'social.backends.google.GoogleOpenId' expected_username = 'FooBar' discovery_body = ''.join([ - '', - '', + '', + '', '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://openid.net/srv/ax/1.0', - '' - 'http://specs.openid.net/extensions/ui/1.0/mode/popup' - '', - 'http://specs.openid.net/extensions/ui/1.0/icon', - 'http://specs.openid.net/extensions/pape/1.0', - 'https://www.google.com/accounts/o8/ud', - '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://openid.net/srv/ax/1.0', - '' - 'http://specs.openid.net/extensions/ui/1.0/mode/popup' - '', - 'http://specs.openid.net/extensions/ui/1.0/icon', - 'http://specs.openid.net/extensions/pape/1.0', - 'https://www.google.com/accounts/o8/ud?source=mail', - '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://openid.net/srv/ax/1.0', - '' - 'http://specs.openid.net/extensions/ui/1.0/mode/popup' - '', - 'http://specs.openid.net/extensions/ui/1.0/icon', - 'http://specs.openid.net/extensions/pape/1.0', - '' - 'https://www.google.com/accounts/o8/ud?source=gmail.com' - '', - '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://openid.net/srv/ax/1.0', - '' - 'http://specs.openid.net/extensions/ui/1.0/mode/popup' - '', - 'http://specs.openid.net/extensions/ui/1.0/icon', - 'http://specs.openid.net/extensions/pape/1.0', - '' - 'https://www.google.com/accounts/o8/ud?source=googlemail.com' - '', - '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://openid.net/srv/ax/1.0', - '' - 'http://specs.openid.net/extensions/ui/1.0/mode/popup' - '', - 'http://specs.openid.net/extensions/ui/1.0/icon', - 'http://specs.openid.net/extensions/pape/1.0', - 'https://www.google.com/accounts/o8/ud?source=profiles', - '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/srv/ax/1.0', + 'http://specs.openid.net/extensions/ui/1.0/mode/popup', + 'http://specs.openid.net/extensions/ui/1.0/icon', + 'http://specs.openid.net/extensions/pape/1.0', + 'https://www.google.com/accounts/o8/ud', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/srv/ax/1.0', + 'http://specs.openid.net/extensions/ui/1.0/mode/popup', + 'http://specs.openid.net/extensions/ui/1.0/icon', + 'http://specs.openid.net/extensions/pape/1.0', + 'https://www.google.com/accounts/o8/ud?source=mail', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/srv/ax/1.0', + 'http://specs.openid.net/extensions/ui/1.0/mode/popup', + 'http://specs.openid.net/extensions/ui/1.0/icon', + 'http://specs.openid.net/extensions/pape/1.0', + 'https://www.google.com/accounts/o8/ud?source=gmail.com', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/srv/ax/1.0', + 'http://specs.openid.net/extensions/ui/1.0/mode/popup', + 'http://specs.openid.net/extensions/ui/1.0/icon', + 'http://specs.openid.net/extensions/pape/1.0', + '', + 'https://www.google.com/accounts/o8/ud?source=googlemail.com', + '', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/srv/ax/1.0', + 'http://specs.openid.net/extensions/ui/1.0/mode/popup', + 'http://specs.openid.net/extensions/ui/1.0/icon', + 'http://specs.openid.net/extensions/pape/1.0', + 'https://www.google.com/accounts/o8/ud?source=profiles', + '', '', - '' + '' ]) server_response = urlencode({ 'janrain_nonce': JANRAIN_NONCE, @@ -284,5 +272,10 @@ def test_revoke_token(self): class GoogleOpenIdConnectTest(OpenIdConnectTestMixin, GoogleOAuth2Test): backend_path = 'social.backends.google.GoogleOpenIdConnect' - user_data_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' + user_data_url = \ + 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' issuer = "accounts.google.com" + + def setUp(self): + GoogleOAuth2Test.setUp(self) + self.access_token_body = self.parse_nonce_and_return_access_token_body diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index 0fd266386..ec3f110dd 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -4,3 +4,4 @@ mock==1.0.1 nose>=1.2.1 requests>=1.1.0 sure==1.2.3 +PyJWT==0.2.1 From 38bdfe28fc897ac1677d36f8b539752448fc19f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 9 Aug 2014 18:10:21 -0300 Subject: [PATCH 315/890] Fix disconnect buttons styles --- examples/django_example/example/templates/home.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 0b8ddf011..3a51c5992 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -15,7 +15,8 @@ .buttons > div:not(:first-child) { margin-top: 10px; border-top: 1px solid #ccc; padding-top: 10px; text-align: center; } .user-details { text-align: center; font-size: 16px; font-weight: bold; } - .disconnect-form { display: inline-block; } + .disconnect-form { padding: 0; margin: 0px 10px; } + .disconnect-form > a { display: block; margin: 5px 0 !important; } @@ -34,8 +35,8 @@

          Python Social Auth

          {% for name, backend in sublist %} {% associated backend %} {% if association %} -
          {% csrf_token %} - + {% csrf_token %} + Disconnect {{ backend|backend_name }} From a5cdf8bacbe67e6aac016515da9bd215b78ca1bc Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 12 Aug 2014 00:31:40 +0200 Subject: [PATCH 316/890] Add pushbullet backends --- social/backends/pushbullet.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 social/backends/pushbullet.py diff --git a/social/backends/pushbullet.py b/social/backends/pushbullet.py new file mode 100755 index 000000000..0568d94b3 --- /dev/null +++ b/social/backends/pushbullet.py @@ -0,0 +1,31 @@ +from social.backends.oauth import BaseOAuth2 +import base64 + +class PushbulletOAuth2(BaseOAuth2): + """pushbullet OAuth authentication backend""" + name = 'pushbullet' + EXTRA_DATA = [('id', 'id')] + ID_KEY = 'username' + AUTHORIZATION_URL = 'https://www.pushbullet.com/authorize' + REQUEST_TOKEN_URL = 'https://api.pushbullet.com/oauth2/token' + ACCESS_TOKEN_URL = 'https://api.pushbullet.com/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + STATE_PARAMETER = False + RESPONSE_TYPE = "code" + + def get_user_details(self, response): + return {'username': response.get('access_token')} + + def get_user_id(self, details, response): + #return details.get(details['iden']); + return self.get_json('https://api.pushbullet.com/v2/users/me', + params={}, headers={'Authorization': "Basic "+base64.b64encode(details['username'])})['iden'] + + + # def user_data(self, access_token, *args, **kwargs): + # """Return user data provided""" + # return self.get_json('https://api.pushbullet.com/v2/users/me', + # params={}, headers={'Authorization': "Basic "+base64.b64encode(access_token)}) + + #'access_token': access_token + #a.eyJ0b2tlbiI6ICJ1akRvanRGSUUwV3Rqelp1SzlKbUhBIiwgInIiOiAicVVHUXBxRUdjMU9Pa2E0M3YyZEpRUjNQVXhpNWZDNFEiLCAidCI6ICJncmFudF92MyJ9.w_1dpcSUVzto1UByzY0901p0d7KSKv5_i1ESmmeH6ww \ No newline at end of file From d53529b57f0a4992889ad490e5314a2244155afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 14 Aug 2014 13:48:23 -0300 Subject: [PATCH 317/890] Fix backend reference. Fixes #350 --- social/apps/django_app/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index b5d3cc22a..c6cd2852c 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -27,7 +27,7 @@ def process_exception(self, request, exception): return if isinstance(exception, SocialAuthBaseException): - backend_name = strategy.backend.name + backend_name = request.backend.name message = self.get_message(request, exception) url = self.get_redirect_uri(request, exception) try: From 70f02246c1f9430e13ec43e0312e89a9a0634d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 15 Aug 2014 22:53:54 -0300 Subject: [PATCH 318/890] Support passwordless schema on mail validation pipeline --- docs/configuration/settings.rst | 8 ++++++++ social/pipeline/mail.py | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 27474f27f..dc30432b8 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -276,6 +276,12 @@ Miscellaneous settings In this case ``foo`` field's value will be stored when user follows this link ``...``. +``SOCIAL_AUTH_PASSWORDLESS = False`` + When this setting is ``True`` and ``social.pipeline.mail.send_validation`` + is enabled, it allows the implementation of a `passwordless authentication + mechanism`_. Example of this implementation can be found at + psa-passwordless_. + Account disconnection --------------------- @@ -291,3 +297,5 @@ using POST. .. _Installation: ../installing.html .. _Backends: ../backends/index.html .. _OAuth: http://oauth.net/ +.. _passwordless authentication mechanism: https://medium.com/@ninjudd/passwords-are-obsolete-9ed56d483eb +.. _psa-passwordless: https://github.com/omab/psa-passwordless diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index 9fbb41645..21083a311 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -3,10 +3,12 @@ @partial -def mail_validation(backend, details, *args, **kwargs): +def mail_validation(backend, details, is_new=False, *args, **kwargs): requires_validation = backend.REQUIRES_EMAIL_VALIDATION or \ backend.setting('FORCE_EMAIL_VALIDATION', False) - if requires_validation and details.get('email'): + send_validation = details.get('email') and \ + (is_new or backend.settings('PASSWORDLESS', False)) + if requires_validation and send_validation: data = backend.strategy.request_data() if 'verification_code' in data: backend.strategy.session_pop('email_validation_address') From 52e09460bb44d6680e0c7c5a3e7bc11f0f3aa054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 16 Aug 2014 00:02:04 -0300 Subject: [PATCH 319/890] PEP8 --- social/backends/pushbullet.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/social/backends/pushbullet.py b/social/backends/pushbullet.py index 0568d94b3..4899ab901 100755 --- a/social/backends/pushbullet.py +++ b/social/backends/pushbullet.py @@ -1,6 +1,8 @@ -from social.backends.oauth import BaseOAuth2 import base64 +from social.backends.oauth import BaseOAuth2 + + class PushbulletOAuth2(BaseOAuth2): """pushbullet OAuth authentication backend""" name = 'pushbullet' @@ -11,21 +13,11 @@ class PushbulletOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://api.pushbullet.com/oauth2/token' ACCESS_TOKEN_METHOD = 'POST' STATE_PARAMETER = False - RESPONSE_TYPE = "code" def get_user_details(self, response): return {'username': response.get('access_token')} - + def get_user_id(self, details, response): - #return details.get(details['iden']); + auth = 'Basic {0}'.format(base64.b64encode(details['username'])) return self.get_json('https://api.pushbullet.com/v2/users/me', - params={}, headers={'Authorization': "Basic "+base64.b64encode(details['username'])})['iden'] - - - # def user_data(self, access_token, *args, **kwargs): - # """Return user data provided""" - # return self.get_json('https://api.pushbullet.com/v2/users/me', - # params={}, headers={'Authorization': "Basic "+base64.b64encode(access_token)}) - - #'access_token': access_token - #a.eyJ0b2tlbiI6ICJ1akRvanRGSUUwV3Rqelp1SzlKbUhBIiwgInIiOiAicVVHUXBxRUdjMU9Pa2E0M3YyZEpRUjNQVXhpNWZDNFEiLCAidCI6ICJncmFudF92MyJ9.w_1dpcSUVzto1UByzY0901p0d7KSKv5_i1ESmmeH6ww \ No newline at end of file + headers={'Authorization': auth})['iden'] From 3d2b5347e00342a7f78f60bff7f9b901e9ec51c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 16 Aug 2014 00:32:57 -0300 Subject: [PATCH 320/890] RTD badge --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 9c66b5795..04781e0c9 100644 --- a/README.rst +++ b/README.rst @@ -17,6 +17,10 @@ for more frameworks and ORMs. .. image:: https://pypip.in/d/python-social-auth/badge.png :target: https://crate.io/packages/python-social-auth?version=latest +.. image:: https://readthedocs.org/projects/pip/badge/?version=latest + :target: https://readthedocs.org/projects/pip/?badge=latest + :alt: Documentation Status + .. contents:: Table of Contents From dc57e331456e5daefa9cd41debc8da2d17d46221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 16 Aug 2014 00:34:44 -0300 Subject: [PATCH 321/890] Link/img change --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 04781e0c9..f845fcb96 100644 --- a/README.rst +++ b/README.rst @@ -17,8 +17,8 @@ for more frameworks and ORMs. .. image:: https://pypip.in/d/python-social-auth/badge.png :target: https://crate.io/packages/python-social-auth?version=latest -.. image:: https://readthedocs.org/projects/pip/badge/?version=latest - :target: https://readthedocs.org/projects/pip/?badge=latest +.. image:: https://readthedocs.org/projects/python-social-auth/badge/?version=latest + :target: https://readthedocs.org/projects/python-social-auth/?badge=latest :alt: Documentation Status .. contents:: Table of Contents From 461b8875403573c11488fafaf8c356aeba1efef1 Mon Sep 17 00:00:00 2001 From: Ross Crawford-d'Heureuse Date: Mon, 18 Aug 2014 11:04:49 +0200 Subject: [PATCH 322/890] added goclio --- social/backends/goclio.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 social/backends/goclio.py diff --git a/social/backends/goclio.py b/social/backends/goclio.py new file mode 100644 index 000000000..147ab50d5 --- /dev/null +++ b/social/backends/goclio.py @@ -0,0 +1,39 @@ +""" +Angel OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/angel.html +""" +from social.backends.oauth import BaseOAuth2 + + +class GoClioOAuth2(BaseOAuth2): + name = 'goclio' + AUTHORIZATION_URL = 'https://app.goclio.com/oauth/authorize/' + ACCESS_TOKEN_METHOD = 'POST' + ACCESS_TOKEN_URL = 'https://app.goclio.com/oauth/token/' + REDIRECT_STATE = False + STATE_PARAMETER = False + + def get_user_details(self, response): + """Return user details from GoClio account""" + account = response.get('account', {}) + user = response.get('user', {}) + + username = user.get('id', None) + email = user.get('email', None) + first_name, last_name = (user.get('first_name', None), user.get('last_name', None)) + fullname = '%s %s' % (first_name, last_name) + + return {'username': username, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, + 'email': email} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://app.goclio.com/api/v2/users/who_am_i', params={ + 'access_token': access_token + }) + + def get_user_id(self, details, response): + return response.get('user', {}).get('id') \ No newline at end of file From a40a7faf63a99af27a2341ef0b675195f8b83883 Mon Sep 17 00:00:00 2001 From: Parker Phinney Date: Mon, 18 Aug 2014 19:54:40 -0700 Subject: [PATCH 323/890] changed default behavior of SESSION_EXPIRATION setting --- docs/configuration/settings.rst | 14 +++++++++----- social/apps/django_app/views.py | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index dc30432b8..846381dca 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -259,11 +259,15 @@ Miscellaneous settings objects, such as ``email``. Set this to a list of fields you only want to set for newly created users and avoid updating on further logins. -``SOCIAL_AUTH_SESSION_EXPIRATION = True`` - Some providers return the time that the access token will live, the value is - stored in ``UserSocialAuth.extra_data`` under the key ``expires``. By default - the current user session is set to expire if this value is present, this - behavior can be disabled by setting. +``SOCIAL_AUTH_SESSION_EXPIRATION = False`` + By default, user session expiration time will be set by your web + framework (in Django, for example, it is set with + SOCIAL_AUTH_SESSION_EXPIRATION). Some providers return the time that the + access token will live, which is stored in ``UserSocialAuth.extra_data`` + under the key ``expires``. Changing this setting to True will override your + web framework's session length setting and set user session lengths to + match the ``expires`` value from the auth provider. + ``SOCIAL_AUTH_OPENID_PAPE_MAX_AUTH_AGE = `` Enable `OpenID PAPE`_ extension support by defining this setting. diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index bf326d13f..0e506c7dc 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -34,8 +34,8 @@ def _do_login(backend, user, social_user): user.backend = '{0}.{1}'.format(backend.__module__, backend.__class__.__name__) login(backend.strategy.request, user) - if backend.setting('SESSION_EXPIRATION', True): - # Set session expiration date if present and not disabled + if backend.setting('SESSION_EXPIRATION', False): + # Set session expiration date if present and enabled # by setting. Use last social-auth instance for current # provider, users can associate several accounts with # a same provider. From 8a0755e66fb9d674172cad48bc470b07064b4642 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Tue, 19 Aug 2014 11:27:27 -0400 Subject: [PATCH 324/890] Fix repository links in thanks document. --- docs/thanks.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/thanks.rst b/docs/thanks.rst index 41678699c..8dd42c9f3 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -111,8 +111,8 @@ let me know and I'll update the list): * sbassi_ -.. _python-social-auth: https:https://github.com/https://github.com/github.comhttps://github.com/omabhttps://github.com/python-social-auth -.. _django-social-auth: https:https://github.com/https://github.com/github.comhttps://github.com/omabhttps://github.com/django-social-auth +.. _python-social-auth: https://github.com/omab/python-social-auth +.. _django-social-auth: https://github.com/omab/django-social-auth .. _kjoconnor: https://github.com/kjoconnor .. _krvss: https://github.com/krvss .. _estebistec: https://github.com/estebistec From 1955bea83e45eaa55b696920d5f3c6f213a380a0 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Thu, 21 Aug 2014 05:55:37 -0700 Subject: [PATCH 325/890] Small grammatical edit --- docs/use_cases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index ac808a46b..8618e4de3 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -108,7 +108,7 @@ Signup by OAuth access_token ---------------------------- It's a common scenario that mobile applications will use an SDK to signup -a user withing the app, but that signup won't be reflected by +a user within the app, but that signup won't be reflected by python-social-auth_ unless the corresponding database entries are created. In order to do so, it's possible to create a view / route that creates those entries by a given ``access_token``. Take the following code for instance (the From f6e2509b778d1162cf74b74dd7c813d599fc51d8 Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Thu, 21 Aug 2014 17:52:23 -0400 Subject: [PATCH 326/890] Split up the Django 1.7+ & South migrations. --- .../default/migrations/0001_initial.py | 299 +++++------------- .../default/south_migrations/0001_initial.py | 147 +++++++++ .../default/south_migrations/__init__.py | 0 3 files changed, 222 insertions(+), 224 deletions(-) create mode 100644 social/apps/django_app/default/south_migrations/0001_initial.py create mode 100644 social/apps/django_app/default/south_migrations/__init__.py diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 20d0eb2eb..60a71dddc 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -3,231 +3,82 @@ import django from django.db import models - - -if django.get_version() < (1,7): - from south.utils import datetime_utils as datetime - from south.db import db - from south.v2 import SchemaMigration - - class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'UserSocialAuth' - db.create_table('social_auth_usersocialauth', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm['auth.User'])), - ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), - )) - db.send_create_signal(u'default', ['UserSocialAuth']) - - # Adding unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] - db.create_unique('social_auth_usersocialauth', ['provider', 'uid']) - - # Adding model 'Nonce' - db.create_table('social_auth_nonce', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('timestamp', self.gf('django.db.models.fields.IntegerField')()), - ('salt', self.gf('django.db.models.fields.CharField')(max_length=65)), - )) - db.send_create_signal(u'default', ['Nonce']) - - # Adding model 'Association' - db.create_table('social_auth_association', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('handle', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('issued', self.gf('django.db.models.fields.IntegerField')()), - ('lifetime', self.gf('django.db.models.fields.IntegerField')()), - ('assoc_type', self.gf('django.db.models.fields.CharField')(max_length=64)), - )) - db.send_create_signal(u'default', ['Association']) - - # Adding model 'Code' - db.create_table('social_auth_code', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), - ('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), - ('verified', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal(u'default', ['Code']) - - # Adding unique constraint on 'Code', fields ['email', 'code'] - db.create_unique('social_auth_code', ['email', 'code']) - - - def backwards(self, orm): - # Removing unique constraint on 'Code', fields ['email', 'code'] - db.delete_unique('social_auth_code', ['email', 'code']) - - # Removing unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] - db.delete_unique('social_auth_usersocialauth', ['provider', 'uid']) - - # Deleting model 'UserSocialAuth' - db.delete_table('social_auth_usersocialauth') - - # Deleting model 'Nonce' - db.delete_table('social_auth_nonce') - - # Deleting model 'Association' - db.delete_table('social_auth_association') - - # Deleting model 'Code' - db.delete_table('social_auth_code') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) +from django.db import migrations + +import social.apps.django_app.default.fields +from django.conf import settings +import social.storage.django_orm + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Association', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('server_url', models.CharField(max_length=255)), + ('handle', models.CharField(max_length=255)), + ('secret', models.CharField(max_length=255)), + ('issued', models.IntegerField()), + ('lifetime', models.IntegerField()), + ('assoc_type', models.CharField(max_length=64)), + ], + options={ + 'db_table': 'social_auth_association', }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + bases=(models.Model, social.storage.django_orm.DjangoAssociationMixin), + ), + migrations.CreateModel( + name='Code', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('email', models.EmailField(max_length=75)), + ('code', models.CharField(db_index=True, max_length=32)), + ('verified', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'social_auth_code', }, - u'default.association': { - 'Meta': {'object_name': 'Association', 'db_table': "'social_auth_association'"}, - 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'issued': ('django.db.models.fields.IntegerField', [], {}), - 'lifetime': ('django.db.models.fields.IntegerField', [], {}), - 'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), + ), + migrations.AlterUniqueTogether( + name='code', + unique_together=set([('email', 'code')]), + ), + migrations.CreateModel( + name='Nonce', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('server_url', models.CharField(max_length=255)), + ('timestamp', models.IntegerField()), + ('salt', models.CharField(max_length=65)), + ], + options={ + 'db_table': 'social_auth_nonce', }, - u'default.code': { - 'Meta': {'unique_together': "(('email', 'code'),)", 'object_name': 'Code', 'db_table': "'social_auth_code'"}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + bases=(models.Model, social.storage.django_orm.DjangoNonceMixin), + ), + migrations.CreateModel( + name='UserSocialAuth', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('provider', models.CharField(max_length=32)), + ('uid', models.CharField(max_length=255)), + ('extra_data', social.apps.django_app.default.fields.JSONField(default='{}')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'social_auth_usersocialauth', }, - u'default.nonce': { - 'Meta': {'object_name': 'Nonce', 'db_table': "'social_auth_nonce'"}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'salt': ('django.db.models.fields.CharField', [], {'max_length': '65'}), - 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'timestamp': ('django.db.models.fields.IntegerField', [], {}) - }, - u'default.usersocialauth': { - 'Meta': {'unique_together': "(('provider', 'uid'),)", 'object_name': 'UserSocialAuth', 'db_table': "'social_auth_usersocialauth'"}, - 'extra_data': ('social.apps.django_app.default.fields.JSONField', [], {'default': "'{}'"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'provider': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'uid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) - } - } - - complete_apps = ['default'] -else: - from django.db import migrations - import social.apps.django_app.default.fields - from django.conf import settings - import social.storage.django_orm - - - class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Association', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('server_url', models.CharField(max_length=255)), - ('handle', models.CharField(max_length=255)), - ('secret', models.CharField(max_length=255)), - ('issued', models.IntegerField()), - ('lifetime', models.IntegerField()), - ('assoc_type', models.CharField(max_length=64)), - ], - options={ - 'db_table': 'social_auth_association', - }, - bases=(models.Model, social.storage.django_orm.DjangoAssociationMixin), - ), - migrations.CreateModel( - name='Code', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('email', models.EmailField(max_length=75)), - ('code', models.CharField(db_index=True, max_length=32)), - ('verified', models.BooleanField(default=False)), - ], - options={ - 'db_table': 'social_auth_code', - }, - bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), - ), - migrations.AlterUniqueTogether( - name='code', - unique_together=set([('email', 'code')]), - ), - migrations.CreateModel( - name='Nonce', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('server_url', models.CharField(max_length=255)), - ('timestamp', models.IntegerField()), - ('salt', models.CharField(max_length=65)), - ], - options={ - 'db_table': 'social_auth_nonce', - }, - bases=(models.Model, social.storage.django_orm.DjangoNonceMixin), - ), - migrations.CreateModel( - name='UserSocialAuth', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('provider', models.CharField(max_length=32)), - ('uid', models.CharField(max_length=255)), - ('extra_data', social.apps.django_app.default.fields.JSONField(default='{}')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'social_auth_usersocialauth', - }, - bases=(models.Model, social.storage.django_orm.DjangoUserMixin), - ), - migrations.AlterUniqueTogether( - name='usersocialauth', - unique_together=set([('provider', 'uid')]), - ), - ] + bases=(models.Model, social.storage.django_orm.DjangoUserMixin), + ), + migrations.AlterUniqueTogether( + name='usersocialauth', + unique_together=set([('provider', 'uid')]), + ), + ] diff --git a/social/apps/django_app/default/south_migrations/0001_initial.py b/social/apps/django_app/default/south_migrations/0001_initial.py new file mode 100644 index 000000000..eb4099304 --- /dev/null +++ b/social/apps/django_app/default/south_migrations/0001_initial.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'UserSocialAuth' + db.create_table('social_auth_usersocialauth', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm['auth.User'])), + ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), + )) + db.send_create_signal(u'default', ['UserSocialAuth']) + + # Adding unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + db.create_unique('social_auth_usersocialauth', ['provider', 'uid']) + + # Adding model 'Nonce' + db.create_table('social_auth_nonce', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('timestamp', self.gf('django.db.models.fields.IntegerField')()), + ('salt', self.gf('django.db.models.fields.CharField')(max_length=65)), + )) + db.send_create_signal(u'default', ['Nonce']) + + # Adding model 'Association' + db.create_table('social_auth_association', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('handle', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('issued', self.gf('django.db.models.fields.IntegerField')()), + ('lifetime', self.gf('django.db.models.fields.IntegerField')()), + ('assoc_type', self.gf('django.db.models.fields.CharField')(max_length=64)), + )) + db.send_create_signal(u'default', ['Association']) + + # Adding model 'Code' + db.create_table('social_auth_code', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), + ('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), + ('verified', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'default', ['Code']) + + # Adding unique constraint on 'Code', fields ['email', 'code'] + db.create_unique('social_auth_code', ['email', 'code']) + + + def backwards(self, orm): + # Removing unique constraint on 'Code', fields ['email', 'code'] + db.delete_unique('social_auth_code', ['email', 'code']) + + # Removing unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + db.delete_unique('social_auth_usersocialauth', ['provider', 'uid']) + + # Deleting model 'UserSocialAuth' + db.delete_table('social_auth_usersocialauth') + + # Deleting model 'Nonce' + db.delete_table('social_auth_nonce') + + # Deleting model 'Association' + db.delete_table('social_auth_association') + + # Deleting model 'Code' + db.delete_table('social_auth_code') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'default.association': { + 'Meta': {'object_name': 'Association', 'db_table': "'social_auth_association'"}, + 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issued': ('django.db.models.fields.IntegerField', [], {}), + 'lifetime': ('django.db.models.fields.IntegerField', [], {}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'default.code': { + 'Meta': {'unique_together': "(('email', 'code'),)", 'object_name': 'Code', 'db_table': "'social_auth_code'"}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + u'default.nonce': { + 'Meta': {'object_name': 'Nonce', 'db_table': "'social_auth_nonce'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'salt': ('django.db.models.fields.CharField', [], {'max_length': '65'}), + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'timestamp': ('django.db.models.fields.IntegerField', [], {}) + }, + u'default.usersocialauth': { + 'Meta': {'unique_together': "(('provider', 'uid'),)", 'object_name': 'UserSocialAuth', 'db_table': "'social_auth_usersocialauth'"}, + 'extra_data': ('social.apps.django_app.default.fields.JSONField', [], {'default': "'{}'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'uid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['default'] diff --git a/social/apps/django_app/default/south_migrations/__init__.py b/social/apps/django_app/default/south_migrations/__init__.py new file mode 100644 index 000000000..e69de29bb From 29a0dadbea65bef5884842675f26bc691c0361c0 Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Thu, 21 Aug 2014 18:51:50 -0400 Subject: [PATCH 327/890] Remove South from mandatory requirements. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 80f3d7145..fa8c222ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.0 six>=1.2.0 -South==0.8.4 From 4f8cacdaa944a1b14a02e1b92ca2afba2e086417 Mon Sep 17 00:00:00 2001 From: Matt Luongo Date: Mon, 25 Aug 2014 21:39:36 -0400 Subject: [PATCH 328/890] Use a more flexible South user migration approach. All credit to @omab and django-social-auth, from which it was cribbed. --- .../default/south_migrations/0001_initial.py | 8 ++++- .../default/south_migrations/__init__.py | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/default/south_migrations/0001_initial.py b/social/apps/django_app/default/south_migrations/0001_initial.py index eb4099304..7373d7fbf 100644 --- a/social/apps/django_app/default/south_migrations/0001_initial.py +++ b/social/apps/django_app/default/south_migrations/0001_initial.py @@ -3,13 +3,18 @@ from south.db import db from south.v2 import SchemaMigration +from .import (get_custom_user_model_for_migrations, + custom_user_frozen_models) + +USER_MODEL = get_custom_user_model_for_migrations() + class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'UserSocialAuth' db.create_table('social_auth_usersocialauth', ( (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm['auth.User'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm[USER_MODEL])), ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), @@ -143,5 +148,6 @@ def backwards(self, orm): 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) } } + models.update(custom_user_frozen_models(USER_MODEL)) complete_apps = ['default'] diff --git a/social/apps/django_app/default/south_migrations/__init__.py b/social/apps/django_app/default/south_migrations/__init__.py index e69de29bb..3b4de5f0c 100644 --- a/social/apps/django_app/default/south_migrations/__init__.py +++ b/social/apps/django_app/default/south_migrations/__init__.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.db.models.loading import get_model + + +def get_custom_user_model_for_migrations(): + user_model = getattr(settings, 'SOCIAL_AUTH_USER_MODEL', None) or \ + getattr(settings, 'AUTH_USER_MODEL', None) or \ + 'auth.User' + if user_model != 'auth.User': + # In case of having a proxy model defined as USER_MODEL + # We use auth.User instead to prevent migration errors + # Since proxy models aren't present in migrations + if get_model(*user_model.split('.'))._meta.proxy: + user_model = 'auth.User' + return user_model + + +def custom_user_frozen_models(user_model): + migration_name = getattr(settings, 'INITIAL_CUSTOM_USER_MIGRATION', + '0001_initial.py') + if user_model != 'auth.User': + from south.migration.base import Migrations + from south.exceptions import NoMigrations + from south.creator.freezer import freeze_apps + user_app, user_model = user_model.split('.') + try: + user_migrations = Migrations(user_app) + except NoMigrations: + extra_model = freeze_apps(user_app) + else: + initial_user_migration = user_migrations.migration(migration_name) + extra_model = initial_user_migration.migration_class().models + else: + extra_model = {} + return extra_model From cfd6439c8f82e48ab6a55c994e6b898d0d905a23 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Wed, 27 Aug 2014 00:46:22 -0400 Subject: [PATCH 329/890] Updated OpenId Connect Test Mixin - Renamed parse_nonce_and_return_access_token_body to access_token_body so that inheriting classes no longer need to set access_token_body - Added method to retrieve id_token so that inheriting classes can modify the id_token response --- social/tests/backends/open_id.py | 33 +++++++++++++++++++--------- social/tests/backends/test_google.py | 4 ---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index 3c608c748..d7517330b 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -138,7 +138,7 @@ def extra_settings(self): }) return settings - def parse_nonce_and_return_access_token_body(self, request, _url, headers): + def access_token_body(self, request, _url, headers): """ Get the nonce from the request parameters, add it to the id_token, and return the complete response. @@ -147,6 +147,24 @@ def parse_nonce_and_return_access_token_body(self, request, _url, headers): body = self.prepare_access_token_body(nonce=nonce) return 200, headers, body + def get_id_token(self, client_key=None, expiration_datetime=None, + issue_datetime=None, nonce=None, issuer=None): + """ + Return the id_token to be added to the access token body. + """ + + id_token = { + 'iss': issuer, + 'nonce': nonce, + 'aud': client_key, + 'azp': client_key, + 'exp': expiration_datetime, + 'iat': issue_datetime, + 'sub': '1234' + } + + return id_token + def prepare_access_token_body(self, client_key=None, client_secret=None, expiration_datetime=None, issue_datetime=None, nonce=None, @@ -171,15 +189,10 @@ def prepare_access_token_body(self, client_key=None, client_secret=None, issue_datetime = issue_datetime or now nonce = nonce or 'a-nonce' issuer = issuer or self.issuer - id_token = { - 'iss': issuer, - 'nonce': nonce, - 'aud': client_key, - 'azp': client_key, - 'exp': timegm(expiration_datetime.utctimetuple()), - 'iat': timegm(issue_datetime.utctimetuple()), - 'sub': '1234', - } + id_token = self.get_id_token( + client_key, timegm(expiration_datetime.utctimetuple()), + timegm(issue_datetime.utctimetuple()), nonce, issuer) + body['id_token'] = jwt.encode(id_token, client_secret).decode('utf-8') return json.dumps(body) diff --git a/social/tests/backends/test_google.py b/social/tests/backends/test_google.py index 0f17b47d7..ece507956 100644 --- a/social/tests/backends/test_google.py +++ b/social/tests/backends/test_google.py @@ -275,7 +275,3 @@ class GoogleOpenIdConnectTest(OpenIdConnectTestMixin, GoogleOAuth2Test): user_data_url = \ 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' issuer = "accounts.google.com" - - def setUp(self): - GoogleOAuth2Test.setUp(self) - self.access_token_body = self.parse_nonce_and_return_access_token_body From 8faed089ea8dad3f4d271b0bcd0211de9ad2cb7b Mon Sep 17 00:00:00 2001 From: Gianluca Pacchiella Date: Wed, 27 Aug 2014 19:03:32 +0200 Subject: [PATCH 330/890] Fix typo --- social/pipeline/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index 21083a311..c52933098 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -7,7 +7,7 @@ def mail_validation(backend, details, is_new=False, *args, **kwargs): requires_validation = backend.REQUIRES_EMAIL_VALIDATION or \ backend.setting('FORCE_EMAIL_VALIDATION', False) send_validation = details.get('email') and \ - (is_new or backend.settings('PASSWORDLESS', False)) + (is_new or backend.setting('PASSWORDLESS', False)) if requires_validation and send_validation: data = backend.strategy.request_data() if 'verification_code' in data: From 71b39a900f180d38966e5209494b027147dc869f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 Aug 2014 17:08:28 -0300 Subject: [PATCH 331/890] PEP8 --- social/backends/goclio.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/social/backends/goclio.py b/social/backends/goclio.py index 147ab50d5..7da0eedf8 100644 --- a/social/backends/goclio.py +++ b/social/backends/goclio.py @@ -1,7 +1,3 @@ -""" -Angel OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/angel.html -""" from social.backends.oauth import BaseOAuth2 @@ -15,12 +11,11 @@ class GoClioOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from GoClio account""" - account = response.get('account', {}) user = response.get('user', {}) - username = user.get('id', None) email = user.get('email', None) - first_name, last_name = (user.get('first_name', None), user.get('last_name', None)) + first_name, last_name = (user.get('first_name', None), + user.get('last_name', None)) fullname = '%s %s' % (first_name, last_name) return {'username': username, @@ -31,9 +26,10 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - return self.get_json('https://app.goclio.com/api/v2/users/who_am_i', params={ - 'access_token': access_token - }) + return self.get_json( + 'https://app.goclio.com/api/v2/users/who_am_i', + params={'access_token': access_token} + ) def get_user_id(self, details, response): - return response.get('user', {}).get('id') \ No newline at end of file + return response.get('user', {}).get('id') From fbc47fe41471dea10cd62e7e2f953553eb84ebda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 Aug 2014 17:23:55 -0300 Subject: [PATCH 332/890] PEP8 --- .../default/migrations/0001_initial.py | 30 ++- .../default/south_migrations/0001_initial.py | 222 ++++++++++++------ 2 files changed, 168 insertions(+), 84 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 60a71dddc..952feb09d 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -1,17 +1,14 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import django -from django.db import models -from django.db import migrations - -import social.apps.django_app.default.fields +from django.db import models, migrations from django.conf import settings + import social.storage.django_orm +import social.apps.django_app.default.fields class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -20,7 +17,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Association', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(serialize=False, primary_key=True, + auto_created=True, verbose_name='ID')), ('server_url', models.CharField(max_length=255)), ('handle', models.CharField(max_length=255)), ('secret', models.CharField(max_length=255)), @@ -31,12 +29,16 @@ class Migration(migrations.Migration): options={ 'db_table': 'social_auth_association', }, - bases=(models.Model, social.storage.django_orm.DjangoAssociationMixin), + bases=( + models.Model, + social.storage.django_orm.DjangoAssociationMixin + ), ), migrations.CreateModel( name='Code', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(serialize=False, primary_key=True, + auto_created=True, verbose_name='ID')), ('email', models.EmailField(max_length=75)), ('code', models.CharField(db_index=True, max_length=32)), ('verified', models.BooleanField(default=False)), @@ -53,7 +55,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Nonce', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(serialize=False, primary_key=True, + auto_created=True, verbose_name='ID')), ('server_url', models.CharField(max_length=255)), ('timestamp', models.IntegerField()), ('salt', models.CharField(max_length=65)), @@ -66,10 +69,13 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserSocialAuth', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(serialize=False, primary_key=True, + auto_created=True, verbose_name='ID')), ('provider', models.CharField(max_length=32)), ('uid', models.CharField(max_length=255)), - ('extra_data', social.apps.django_app.default.fields.JSONField(default='{}')), + ('extra_data', + social.apps.django_app.default.fields.JSONField(default='{}') + ), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), ], options={ diff --git a/social/apps/django_app/default/south_migrations/0001_initial.py b/social/apps/django_app/default/south_migrations/0001_initial.py index 7373d7fbf..e96aeca92 100644 --- a/social/apps/django_app/default/south_migrations/0001_initial.py +++ b/social/apps/django_app/default/south_migrations/0001_initial.py @@ -1,68 +1,87 @@ # -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime from south.db import db from south.v2 import SchemaMigration -from .import (get_custom_user_model_for_migrations, - custom_user_frozen_models) +from . import get_custom_user_model_for_migrations, custom_user_frozen_models + USER_MODEL = get_custom_user_model_for_migrations() -class Migration(SchemaMigration): +class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'UserSocialAuth' db.create_table('social_auth_usersocialauth', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='social_auth', to=orm[USER_MODEL])), - ('provider', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('uid', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('extra_data', self.gf('social.apps.django_app.default.fields.JSONField')(default='{}')), + (u'id', self.gf('django.db.models.fields.AutoField')( + primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')( + related_name='social_auth', to=orm[USER_MODEL])), + ('provider', self.gf('django.db.models.fields.CharField')( + max_length=32)), + ('uid', self.gf('django.db.models.fields.CharField')( + max_length=255)), + ('extra_data', self.gf( + 'social.apps.django_app.default.fields.JSONField' + )(default='{}')), )) db.send_create_signal(u'default', ['UserSocialAuth']) - # Adding unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + # Adding unique constraint on 'UserSocialAuth', + # fields ['provider', 'uid'] db.create_unique('social_auth_usersocialauth', ['provider', 'uid']) # Adding model 'Nonce' db.create_table('social_auth_nonce', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + (u'id', self.gf('django.db.models.fields.AutoField')( + primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')( + max_length=255)), ('timestamp', self.gf('django.db.models.fields.IntegerField')()), - ('salt', self.gf('django.db.models.fields.CharField')(max_length=65)), + ('salt', self.gf('django.db.models.fields.CharField')( + max_length=65)), )) db.send_create_signal(u'default', ['Nonce']) # Adding model 'Association' db.create_table('social_auth_association', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('server_url', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('handle', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)), + (u'id', self.gf('django.db.models.fields.AutoField')( + primary_key=True)), + ('server_url', self.gf('django.db.models.fields.CharField')( + max_length=255)), + ('handle', self.gf('django.db.models.fields.CharField')( + max_length=255)), + ('secret', self.gf('django.db.models.fields.CharField')( + max_length=255)), ('issued', self.gf('django.db.models.fields.IntegerField')()), ('lifetime', self.gf('django.db.models.fields.IntegerField')()), - ('assoc_type', self.gf('django.db.models.fields.CharField')(max_length=64)), + ('assoc_type', self.gf('django.db.models.fields.CharField')( + max_length=64)), )) db.send_create_signal(u'default', ['Association']) # Adding model 'Code' db.create_table('social_auth_code', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), - ('code', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), - ('verified', self.gf('django.db.models.fields.BooleanField')(default=False)), + (u'id', self.gf('django.db.models.fields.AutoField')( + primary_key=True)), + ('email', self.gf('django.db.models.fields.EmailField')( + max_length=75)), + ('code', self.gf('django.db.models.fields.CharField')( + max_length=32, + db_index=True)), + ('verified', self.gf('django.db.models.fields.BooleanField')( + default=False)), )) db.send_create_signal(u'default', ['Code']) # Adding unique constraint on 'Code', fields ['email', 'code'] db.create_unique('social_auth_code', ['email', 'code']) - def backwards(self, orm): # Removing unique constraint on 'Code', fields ['email', 'code'] db.delete_unique('social_auth_code', ['email', 'code']) - # Removing unique constraint on 'UserSocialAuth', fields ['provider', 'uid'] + # Removing unique constraint on 'UserSocialAuth', + # fields ['provider', 'uid'] db.delete_unique('social_auth_usersocialauth', ['provider', 'uid']) # Deleting model 'UserSocialAuth' @@ -77,75 +96,134 @@ def backwards(self, orm): # Deleting model 'Code' db.delete_table('social_auth_code') - models = { u'auth.group': { 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], + {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', + [], {'to': u"orm['auth.Permission']", + 'symmetrical': 'False', 'blank': 'True'}) }, u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + 'Meta': { + 'ordering': + "(u'content_type__app_label', " + "u'content_type__model', u'codename')", + 'unique_together': "((u'content_type', u'codename'),)", + 'object_name': 'Permission' + }, + 'codename': ('django.db.models.fields.CharField', [], + {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], + {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], + {'max_length': '50'}) }, u'auth.user': { 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + 'date_joined': ('django.db.models.fields.DateTimeField', [], + {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], + {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], + {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], + {'symmetrical': 'False', 'related_name': "u'user_set'", + 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], + {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], + {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], + {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], + {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], + {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], + {'max_length': '128'}), + 'user_permissions': ( + 'django.db.models.fields.related.ManyToManyField', [], + {'symmetrical': 'False', 'related_name': "u'user_set'", + 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], + {'unique': 'True', 'max_length': '30'}) }, u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + 'Meta': {'ordering': "('name',)", + 'unique_together': "(('app_label', 'model'),)", + 'object_name': 'ContentType', + 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], + {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], + {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], + {'max_length': '100'}) }, u'default.association': { - 'Meta': {'object_name': 'Association', 'db_table': "'social_auth_association'"}, - 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'Meta': {'object_name': 'Association', + 'db_table': "'social_auth_association'"}, + 'assoc_type': ('django.db.models.fields.CharField', [], + {'max_length': '64'}), + 'handle': ('django.db.models.fields.CharField', [], + {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), 'issued': ('django.db.models.fields.IntegerField', [], {}), 'lifetime': ('django.db.models.fields.IntegerField', [], {}), - 'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + 'secret': ('django.db.models.fields.CharField', [], + {'max_length': '255'}), + 'server_url': ('django.db.models.fields.CharField', [], + {'max_length': '255'}) }, u'default.code': { - 'Meta': {'unique_together': "(('email', 'code'),)", 'object_name': 'Code', 'db_table': "'social_auth_code'"}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + 'Meta': {'unique_together': "(('email', 'code'),)", + 'object_name': 'Code', 'db_table': "'social_auth_code'"}, + 'code': ('django.db.models.fields.CharField', [], + {'max_length': '32', 'db_index': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], + {'max_length': '75'}), + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'verified': ('django.db.models.fields.BooleanField', [], + {'default': 'False'}) }, u'default.nonce': { - 'Meta': {'object_name': 'Nonce', 'db_table': "'social_auth_nonce'"}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'salt': ('django.db.models.fields.CharField', [], {'max_length': '65'}), - 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'Meta': {'object_name': 'Nonce', + 'db_table': "'social_auth_nonce'"}, + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'salt': ('django.db.models.fields.CharField', [], + {'max_length': '65'}), + 'server_url': ('django.db.models.fields.CharField', [], + {'max_length': '255'}), 'timestamp': ('django.db.models.fields.IntegerField', [], {}) }, u'default.usersocialauth': { - 'Meta': {'unique_together': "(('provider', 'uid'),)", 'object_name': 'UserSocialAuth', 'db_table': "'social_auth_usersocialauth'"}, - 'extra_data': ('social.apps.django_app.default.fields.JSONField', [], {'default': "'{}'"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'provider': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'uid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'social_auth'", 'to': u"orm['auth.User']"}) + 'Meta': {'unique_together': "(('provider', 'uid'),)", + 'object_name': 'UserSocialAuth', + 'db_table': "'social_auth_usersocialauth'"}, + 'extra_data': ('social.apps.django_app.default.fields.JSONField', + [], {'default': "'{}'"}), + u'id': ('django.db.models.fields.AutoField', [], + {'primary_key': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], + {'max_length': '32'}), + 'uid': ('django.db.models.fields.CharField', [], + {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], + {'related_name': "'social_auth'", + 'to': u"orm['auth.User']"}) } } models.update(custom_user_frozen_models(USER_MODEL)) From 92219cf7f0aac3e3a2313372ecb3bf63f5ba0185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 31 Aug 2014 22:32:54 -0300 Subject: [PATCH 333/890] Enable state parameter for angel.co and spotify.com backends. Fixes #367 --- social/backends/angel.py | 1 - social/backends/spotify.py | 1 - 2 files changed, 2 deletions(-) diff --git a/social/backends/angel.py b/social/backends/angel.py index ad364972f..4f0a7082c 100644 --- a/social/backends/angel.py +++ b/social/backends/angel.py @@ -11,7 +11,6 @@ class AngelOAuth2(BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' ACCESS_TOKEN_URL = 'https://angel.co/api/oauth/token/' REDIRECT_STATE = False - STATE_PARAMETER = False def get_user_details(self, response): """Return user details from Angel account""" diff --git a/social/backends/spotify.py b/social/backends/spotify.py index e3c9be947..0c2d3c834 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -16,7 +16,6 @@ class SpotifyOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False - STATE_PARAMETER = False def auth_headers(self): return { From 1b56357c5a8365310507e89385457b1d3df9781b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 3 Sep 2014 20:40:35 -0300 Subject: [PATCH 334/890] Added commets detailing pipeline functionality. Refs #361 --- docs/pipeline.rst | 43 +++++++++++++++++++++++++++++++++++++ social/pipeline/__init__.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index ee61806a3..6369779ee 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -30,17 +30,51 @@ user instances and gathers basic data from providers. The default pipeline is composed by:: ( + # Get the information we can about the user and return it in a simple + # format to create the user instance later. On some cases the details are + # already part of the auth response from the provider, but sometimes this + # could hit a provider API. 'social.pipeline.social_auth.social_details', + + # Get the social uid from whichever service we're authing thru. The uid is + # the unique identifier of the given user in the provider. 'social.pipeline.social_auth.social_uid', + + # Verifies that the current auth process is valid within the current + # project, this is were emails and domains whitelists are applied (if + # defined). 'social.pipeline.social_auth.auth_allowed', + + # Checks if the current social-account is already associated in the site. 'social.pipeline.social_auth.social_user', + + # Make up a username for this person, appends a random string at the end if + # there's any collision. 'social.pipeline.user.get_username', + + # Send a validation email to the user to verify its email address. + # Disabled by default. + # 'social.pipeline.mail.mail_validation', + + # Associates the current social details with another user account with + # a similar email address. Disabled by default. + # 'social.pipeline.social_auth.associate_by_email', + + # Create a user account if we haven't found one yet. 'social.pipeline.user.create_user', + + # Create the record that associated the social account with this user. 'social.pipeline.social_auth.associate_user', + + # Populate the extra_data field in the social record with the values + # specified by settings (and the default ones like access_token, etc). 'social.pipeline.social_auth.load_extra_data', + + # Update the user record with any changed info from the auth service. 'social.pipeline.user.user_details' ) + It's possible to override it by defining the setting ``SOCIAL_AUTH_PIPELINE``, for example a pipeline that won't create users, just accept already registered ones would look like this:: @@ -98,9 +132,18 @@ password in your pipeline function. Check *Partial Pipeline* below. In order to override the disconnection pipeline, just define the setting:: SOCIAL_AUTH_DISCONNECT_PIPELINE = ( + # Verifies that the social association can be disconnected from the current + # user (ensure that the user login mechanism is not compromised by this + # disconnection). 'social.pipeline.disconnect.allowed_to_disconnect', + + # Collects the social associations to disconnect. 'social.pipeline.disconnect.get_entries', + + # Revoke any access_token when possible. 'social.pipeline.disconnect.revoke_tokens', + + # Removes the social associations. 'social.pipeline.disconnect.disconnect' ) diff --git a/social/pipeline/__init__.py b/social/pipeline/__init__.py index ecef7badd..132a590f7 100644 --- a/social/pipeline/__init__.py +++ b/social/pipeline/__init__.py @@ -1,20 +1,59 @@ DEFAULT_AUTH_PIPELINE = ( + # Get the information we can about the user and return it in a simple + # format to create the user instance later. On some cases the details are + # already part of the auth response from the provider, but sometimes this + # could hit a provider API. 'social.pipeline.social_auth.social_details', + + # Get the social uid from whichever service we're authing thru. The uid is + # the unique identifier of the given user in the provider. 'social.pipeline.social_auth.social_uid', + + # Verifies that the current auth process is valid within the current + # project, this is were emails and domains whitelists are applied (if + # defined). 'social.pipeline.social_auth.auth_allowed', + + # Checks if the current social-account is already associated in the site. 'social.pipeline.social_auth.social_user', + + # Make up a username for this person, appends a random string at the end if + # there's any collision. 'social.pipeline.user.get_username', + + # Send a validation email to the user to verify its email address. # 'social.pipeline.mail.mail_validation', + + # Associates the current social details with another user account with + # a similar email address. # 'social.pipeline.social_auth.associate_by_email', + + # Create a user account if we haven't found one yet. 'social.pipeline.user.create_user', + + # Create the record that associated the social account with this user. 'social.pipeline.social_auth.associate_user', + + # Populate the extra_data field in the social record with the values + # specified by settings (and the default ones like access_token, etc). 'social.pipeline.social_auth.load_extra_data', + + # Update the user record with any changed info from the auth service. 'social.pipeline.user.user_details' ) DEFAULT_DISCONNECT_PIPELINE = ( + # Verifies that the social association can be disconnected from the current + # user (ensure that the user login mechanism is not compromised by this + # disconnection). 'social.pipeline.disconnect.allowed_to_disconnect', + + # Collects the social associations to disconnect. 'social.pipeline.disconnect.get_entries', + + # Revoke any access_token when possible. 'social.pipeline.disconnect.revoke_tokens', + + # Removes the social associations. 'social.pipeline.disconnect.disconnect' ) From c7a8173aa2b69d82986bdfe361c656fda3f8a9f7 Mon Sep 17 00:00:00 2001 From: Caio Ariede Date: Sun, 7 Sep 2014 23:28:18 -0300 Subject: [PATCH 335/890] Support for MineID.org --- docs/backends/index.rst | 1 + docs/backends/mineid.rst | 25 +++++++++++ docs/intro.rst | 2 + examples/cherrypy_example/templates/home.html | 1 + examples/django_example/example/settings.py | 1 + .../django_me_example/example/settings.py | 1 + .../example/templates/home.html | 1 + examples/flask_example/settings.py | 1 + examples/flask_example/templates/home.html | 1 + examples/flask_me_example/settings.py | 1 + examples/pyramid_example/example/settings.py | 1 + examples/tornado_example/settings.py | 1 + examples/tornado_example/templates/home.html | 1 + examples/webpy_example/app.py | 1 + examples/webpy_example/templates/home.html | 1 + social/backends/mineid.py | 41 +++++++++++++++++++ social/tests/backends/test_mineid.py | 26 ++++++++++++ 17 files changed, 107 insertions(+) create mode 100644 docs/backends/mineid.rst create mode 100644 social/backends/mineid.py create mode 100644 social/tests/backends/test_mineid.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index cc95ca979..b1dcfc1f7 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -81,6 +81,7 @@ Social backends mailru mapmyfitness mendeley + mineid mixcloud moves odnoklassnikiru diff --git a/docs/backends/mineid.rst b/docs/backends/mineid.rst new file mode 100644 index 000000000..c7b0557c1 --- /dev/null +++ b/docs/backends/mineid.rst @@ -0,0 +1,25 @@ +MineID +====== + +MineID works similar to Facebook (OAuth). + +- Register a new application at `MineID.org`_, set the callback URL to + ``http://example.com/complete/mineid/`` replacing ``example.com`` with your + domain. + +- Fill ``Client ID`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_MINEID_KEY = '' + SOCIAL_AUTH_MINEID_SECRET = '' + + +Self-hosted MineID +------------------ + +Since MineID is an Open Source software and can be self-hosted, you can +change settings to point to your instance:: + + SOCIAL_AUTH_MINEID_HOST = 'www.your-mineid-instance.com' + SOCIAL_AUTH_MINEID_SCHEME = 'https' # or 'http' + +.. _MineID.org: https://www.mineid.org/ diff --git a/docs/intro.rst b/docs/intro.rst index eb641a7e7..623e58043 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -57,6 +57,7 @@ or extend current one): * Live_ OAuth2 * Livejournal_ OpenId * Mailru_ OAuth2 + * MineID_ OAuth2 * Mixcloud_ OAuth2 * `Mozilla Persona`_ * Odnoklassniki_ OAuth2 and Application Auth @@ -135,6 +136,7 @@ section. .. _Live: https://www.live.com .. _Livejournal: http://livejournal.com .. _Mailru: https://mail.ru +.. _MineID: https://www.mineid.org .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _Odnoklassniki: http://www.odnoklassniki.ru diff --git a/examples/cherrypy_example/templates/home.html b/examples/cherrypy_example/templates/home.html index 072375dd6..91302d26d 100644 --- a/examples/cherrypy_example/templates/home.html +++ b/examples/cherrypy_example/templates/home.html @@ -34,6 +34,7 @@ Xing OAuth
          Yandex OAuth2
          Podio OAuth2
          +MineID OAuth2
          diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 9243408cf..ec330960a 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -157,6 +157,7 @@ 'social.backends.mailru.MailruOAuth2', 'social.backends.mendeley.MendeleyOAuth', 'social.backends.mendeley.MendeleyOAuth2', + 'social.backends.mineid.MineIDOAuth2', 'social.backends.mixcloud.MixcloudOAuth2', 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', 'social.backends.open_id.OpenIdAuth', diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 30bd3561a..f116e62b7 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -164,6 +164,7 @@ 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', 'social.backends.douban.DoubanOAuth2', + 'social.backends.mineid.MineIDOAuth2', 'social.backends.mixcloud.MixcloudOAuth2', 'social.backends.rdio.RdioOAuth1', 'social.backends.rdio.RdioOAuth2', diff --git a/examples/django_me_example/example/templates/home.html b/examples/django_me_example/example/templates/home.html index 066a44307..5184b12c6 100644 --- a/examples/django_me_example/example/templates/home.html +++ b/examples/django_me_example/example/templates/home.html @@ -37,6 +37,7 @@ Xing OAuth
          Yandex OAuth2
          Douban OAuth2
          +MineID OAuth2
          Mixcloud OAuth2
          Rdio OAuth2
          Rdio OAuth1
          diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index f2cd91ad5..839e0098d 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -54,4 +54,5 @@ 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', + 'social.backends.mineid.MineIDOAuth2', ) diff --git a/examples/flask_example/templates/home.html b/examples/flask_example/templates/home.html index bd91538d9..1c7f9bcef 100644 --- a/examples/flask_example/templates/home.html +++ b/examples/flask_example/templates/home.html @@ -35,6 +35,7 @@ Xing OAuth
          Yandex OAuth2
          Podio OAuth2
          +MineID OAuth2
          diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py index 6bd214770..141b39323 100644 --- a/examples/flask_me_example/settings.py +++ b/examples/flask_me_example/settings.py @@ -55,4 +55,5 @@ 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', + 'social.backends.mineid.MineIDOAuth2', ) diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index ece04853b..e96708c6d 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -43,6 +43,7 @@ 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', + 'social.backends.mineid.MineIDOAuth2', ) } diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index 8ac85f9dc..978a2c7ea 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -42,6 +42,7 @@ 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', + 'social.backends.mineid.MineIDOAuth2', ) from local_settings import * diff --git a/examples/tornado_example/templates/home.html b/examples/tornado_example/templates/home.html index daa33d21f..8c8c766d9 100644 --- a/examples/tornado_example/templates/home.html +++ b/examples/tornado_example/templates/home.html @@ -35,6 +35,7 @@ Xing OAuth
          Yandex OAuth2
          Podio OAuth2
          +MineID OAuth2
          diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 1136b7692..9e8456ac4 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -54,6 +54,7 @@ 'social.backends.xing.XingOAuth', 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', + 'social.backends.mineid.MineIDOAuth2', ) web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/' diff --git a/examples/webpy_example/templates/home.html b/examples/webpy_example/templates/home.html index 042da0ff5..e83b5a4fc 100644 --- a/examples/webpy_example/templates/home.html +++ b/examples/webpy_example/templates/home.html @@ -35,6 +35,7 @@ Xing OAuth
          Yandex OAuth2
          Podio OAuth2
          +MineID OAuth2
          diff --git a/social/backends/mineid.py b/social/backends/mineid.py new file mode 100644 index 000000000..7bfe97f1a --- /dev/null +++ b/social/backends/mineid.py @@ -0,0 +1,41 @@ +import json +import urllib + +from social.backends.oauth import BaseOAuth2 + + +class MineIDOAuth2(BaseOAuth2): + """MineID OAuth2 authentication backend""" + name = 'mineid' + _AUTHORIZATION_URL = '%(scheme)s://%(host)s/oauth/authorize' + _ACCESS_TOKEN_URL = '%(scheme)s://%(host)s/oauth/access_token' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' + EXTRA_DATA = [ + ] + + def get_user_details(self, response): + """Return user details""" + return {'email': response.get('email'), + 'username': response.get('email')} + + def user_data(self, access_token, *args, **kwargs): + return self._user_data(access_token) + + def _user_data(self, access_token, path=None): + url = '%(scheme)s://%(host)s/api/user' % self.get_mineid_url_params() + return self.get_json(url, params={'access_token': access_token}) + + @property + def AUTHORIZATION_URL(self): + return self._AUTHORIZATION_URL % self.get_mineid_url_params() + + @property + def ACCESS_TOKEN_URL(self): + return self._ACCESS_TOKEN_URL % self.get_mineid_url_params() + + def get_mineid_url_params(self): + return { + 'host': self.setting('HOST', 'www.mineid.org'), + 'scheme': self.setting('SCHEME', 'https'), + } diff --git a/social/tests/backends/test_mineid.py b/social/tests/backends/test_mineid.py new file mode 100644 index 000000000..6d55d4d79 --- /dev/null +++ b/social/tests/backends/test_mineid.py @@ -0,0 +1,26 @@ +import json + +from social.p3 import urlencode +from social.exceptions import AuthUnknownError + +from social.tests.backends.oauth import OAuth2Test + + +class MineIDOAuth2Test(OAuth2Test): + backend_path = 'social.backends.mineid.MineIDOAuth2' + user_data_url = 'https://www.mineid.org/api/user' + expected_username = 'foo@bar.com' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'email': 'foo@bar.com', + 'primary_profile': None, + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From b0c880b7f8e76bdda21622dfef76fb64d328fb4d Mon Sep 17 00:00:00 2001 From: Amol Kher Date: Sun, 7 Sep 2014 23:31:37 -0700 Subject: [PATCH 336/890] Jawbone needs params instead of data as requests --- social/backends/jawbone.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index 572dd162e..e0fe9d440 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -51,3 +51,24 @@ def process_error(self, data): error )) return super(JawboneOAuth2, self).process_error(data) + + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + self.process_error(self.data) + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + params=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + raise + except KeyError: + raise AuthUnknownError(self) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) From ea281303bca5164bc187d9d2f0b9c5126cb49aa2 Mon Sep 17 00:00:00 2001 From: Tsung Hung Date: Tue, 9 Sep 2014 09:13:48 -0700 Subject: [PATCH 337/890] updated the docs to add migrations for 1.7 while updated a constant so the warning message does not appear when running command line --- examples/django_example/example/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 62daabf7c..4859359f1 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -229,6 +229,8 @@ #'social.pipeline.debug.debug' ) +TEST_RUNNER = 'django.test.runner.DiscoverRunner' + # SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['first_name', 'last_name', 'email', # 'username'] From 03d0f4d3733de2ac3aa7105b8051080ade9db473 Mon Sep 17 00:00:00 2001 From: Tsung Hung Date: Tue, 9 Sep 2014 09:15:16 -0700 Subject: [PATCH 338/890] updated the docs to add migrations for 1.7 while updated a constant so the warning message does not appear when running command line --- docs/configuration/django.rst | 6 ++++++ examples/django_me_example/example/settings.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index 9c03d562d..ea1975042 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -36,6 +36,12 @@ Also ensure to define the MongoEngine_ storage setting:: Database -------- +(For Django 1.7 and higher) sync database to create needed models:: + + ./manage.py makemigrations + + + Sync database to create needed models:: ./manage.py syncdb diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 30bd3561a..a32ceed2a 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -211,6 +211,8 @@ 'social.pipeline.user.user_details' ) +TEST_RUNNER = 'django.test.runner.DiscoverRunner' + try: from example.local_settings import * except ImportError: From 76603ef7ece8454c3a985ade58af5f64f430ee72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 12:26:00 -0300 Subject: [PATCH 339/890] Restore @strategy decorator with warning message --- social/apps/cherrypy_app/utils.py | 10 ++++++++-- social/apps/django_app/utils.py | 7 +++++++ social/apps/flask_app/utils.py | 7 +++++++ social/apps/pyramid_app/utils.py | 7 +++++++ social/apps/tornado_app/utils.py | 7 +++++++ social/apps/webpy_app/utils.py | 9 ++++++++- 6 files changed, 44 insertions(+), 3 deletions(-) diff --git a/social/apps/cherrypy_app/utils.py b/social/apps/cherrypy_app/utils.py index afdd1659c..70e89c919 100644 --- a/social/apps/cherrypy_app/utils.py +++ b/social/apps/cherrypy_app/utils.py @@ -1,7 +1,8 @@ -import cherrypy - +import warnings from functools import wraps +import cherrypy + from social.utils import setting_name, module_member from social.strategies.utils import get_strategy from social.backends.utils import get_backend, user_backends_data @@ -43,3 +44,8 @@ def backends(user): Will return the output of social.backends.utils.user_backends_data.""" return user_backends_data(user, get_helper('AUTHENTICATION_BACKENDS'), module_member(get_helper('STORAGE'))) + + +def strategy(*args, **kwargs): + warnings.warn('@strategy decorator is deprecated, use @psa instead') + return psa(*args, **kwargs) diff --git a/social/apps/django_app/utils.py b/social/apps/django_app/utils.py index b45b62e46..ff950a9c7 100644 --- a/social/apps/django_app/utils.py +++ b/social/apps/django_app/utils.py @@ -1,3 +1,5 @@ +import warnings + from functools import wraps from django.conf import settings @@ -66,3 +68,8 @@ def authenticate(self, *args, **kwargs): def get_user(self, user_id): return Strategy(storage=Storage).get_user(user_id) + + +def strategy(*args, **kwargs): + warnings.warn('@strategy decorator is deprecated, use @psa instead') + return psa(*args, **kwargs) diff --git a/social/apps/flask_app/utils.py b/social/apps/flask_app/utils.py index 8510afa92..2e77bcaf6 100644 --- a/social/apps/flask_app/utils.py +++ b/social/apps/flask_app/utils.py @@ -1,3 +1,5 @@ +import warnings + from functools import wraps from flask import current_app, url_for, g @@ -44,3 +46,8 @@ def wrapper(backend, *args, **kwargs): return func(backend, *args, **kwargs) return wrapper return decorator + + +def strategy(*args, **kwargs): + warnings.warn('@strategy decorator is deprecated, use @psa instead') + return psa(*args, **kwargs) diff --git a/social/apps/pyramid_app/utils.py b/social/apps/pyramid_app/utils.py index 64d3dc7ef..832bc5b8a 100644 --- a/social/apps/pyramid_app/utils.py +++ b/social/apps/pyramid_app/utils.py @@ -1,3 +1,5 @@ +import warnings + from functools import wraps from pyramid.threadlocal import get_current_registry @@ -69,3 +71,8 @@ def backends(request, user): user, get_helper('AUTHENTICATION_BACKENDS'), storage ) } + + +def strategy(*args, **kwargs): + warnings.warn('@strategy decorator is deprecated, use @psa instead') + return psa(*args, **kwargs) diff --git a/social/apps/tornado_app/utils.py b/social/apps/tornado_app/utils.py index 6026a44d9..04e4a13dc 100644 --- a/social/apps/tornado_app/utils.py +++ b/social/apps/tornado_app/utils.py @@ -1,3 +1,5 @@ +import warnings + from functools import wraps from social.utils import setting_name @@ -40,3 +42,8 @@ def wrapper(self, backend, *args, **kwargs): return func(self, backend, *args, **kwargs) return wrapper return decorator + + +def strategy(*args, **kwargs): + warnings.warn('@strategy decorator is deprecated, use @psa instead') + return psa(*args, **kwargs) diff --git a/social/apps/webpy_app/utils.py b/social/apps/webpy_app/utils.py index c42fe9f4b..b02035e93 100644 --- a/social/apps/webpy_app/utils.py +++ b/social/apps/webpy_app/utils.py @@ -1,7 +1,9 @@ -import web +import warnings from functools import wraps +import web + from social.utils import setting_name, module_member from social.backends.utils import get_backend, user_backends_data from social.strategies.utils import get_strategy @@ -60,3 +62,8 @@ def login_redirect(): 'REDIRECT_FIELD_VALUE': value, 'REDIRECT_QUERYSTRING': value and ('next=' + value) or '' } + + +def strategy(*args, **kwargs): + warnings.warn('@strategy decorator is deprecated, use @psa instead') + return psa(*args, **kwargs) From 2fce5f92145280356ab713189ab82bb78f5f4fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 12:27:24 -0300 Subject: [PATCH 340/890] v0.2.0 --- Changelog | 474 +++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 475 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 5b838b815..3396c79c4 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,477 @@ +2014-09-11 v0.2.0 +================= + + * 2014-09-11 Matías Aguirre + v0.2.0 + + * 2014-09-11 Matías Aguirre + Restore @strategy decorator with warning message + + * 2014-09-03 Matías Aguirre + Added commets detailing pipeline functionality. Refs #361 + + * 2014-08-31 Matías Aguirre + Enable state parameter for angel.co and spotify.com backends. Fixes #367 + + * 2014-08-29 Matías Aguirre + PEP8 + + * 2014-08-29 Matías Aguirre + PEP8 + + * 2014-08-27 Gianluca Pacchiella + Fix typo + + * 2014-08-27 Clinton Blackburn + Updated OpenId Connect Test Mixin + + * 2014-08-25 Matt Luongo + Use a more flexible South user migration approach. + + * 2014-08-21 Matt Luongo + Remove South from mandatory requirements. + + * 2014-08-21 Matt Luongo + Split up the Django 1.7+ & South migrations. + + * 2014-08-21 Max Nanis + Small grammatical edit + + * 2014-08-19 Martey Dodoo + Fix repository links in thanks document. + + * 2014-08-18 Parker Phinney + changed default behavior of SESSION_EXPIRATION setting + + * 2014-08-18 Ross Crawford-d'Heureuse + added goclio + + * 2014-08-16 Matías Aguirre + Link/img change + + * 2014-08-16 Matías Aguirre + RTD badge + + * 2014-08-16 Matías Aguirre + PEP8 + + * 2014-08-15 Matías Aguirre + Support passwordless schema on mail validation pipeline + + * 2014-08-14 Matías Aguirre + Fix backend reference. Fixes #350 + + * 2014-08-12 = <=> + Add pushbullet backends + + * 2014-08-09 Matías Aguirre + Fix disconnect buttons styles + + * 2014-08-09 Matías Aguirre + PEP8 and fixed tests. Refs #348 + + * 2014-08-08 Clinton Blackburn + Added Open ID Connect base backend + + * 2014-08-08 Josh Probst + numeric index for format + + * 2014-08-07 Matías Aguirre + Fix user syncdb. Refs #342 + + * 2014-08-07 Matías Aguirre + Simplify moves backend code and add documentation. Refs #307 + + * 2014-08-05 Vadym Petrychenko + Update vk.rst + + * 2014-08-02 Matías Aguirre + Landscape conf + + * 2014-08-02 Matías Aguirre + Support redirect_state in OAuth1 backends too (enable twitter by default). + Refs #338 + + * 2014-08-02 Matías Aguirre + Enable DropboxOAuth2 on example app + + * 2014-07-29 Chris Lamb + Also populate Strava name from 'lastname' attribute: + + * 2014-07-29 Chris Lamb + Correct reference to 'firstname' when populating forenames from Strava. + + * 2014-07-29 Chris Lamb + Correct Stava scoping/permissions example. + + * 2014-07-28 Chris Martin + Clean up language in social/tests/README.rst + + * 2014-07-27 Matías Aguirre + Docs about writing custom pipeline functions + + * 2014-07-24 Matt Luongo + Fix an import issue in the Django migrations. + + * 2014-07-24 Matt Luongo + List test requirements. + + * 2014-07-24 Matt Luongo + Support South and Django 1.7+ migrations. + + * 2014-07-19 Matías Aguirre + Github for teams backend. Refs #329 + + * 2014-07-16 Nick Sandford + Fixed #327 -- Changed access token method on backend. + + * 2014-07-15 David Grant + Slight retouch to spelling and wordage. + + * 2014-07-15 David Grant + Minor typo. + + * 2014-07-08 Matías Aguirre + Fix FK field descriptor for admin queries. Closes #322 + + * 2014-07-07 Matt Luongo + Use South instead of Django 1.7 migrations. + + * 2014-07-07 Matías Aguirre + Simple makefile for local tasks + + * 2014-07-07 Matías Aguirre + Document django session migration script when moving from + django-social-auth to python-social-auth. Refs #320 + + * 2014-07-07 Matías Aguirre + Make user-agent setting available for all backends. Refs #317 + + * 2014-07-04 Harz-FEAR + fix for AssertionError in pyramid + + * 2014-07-01 Ondrej Slinták + Added Django 1.7 migrations + + * 2014-07-01 davidhubbard + fix PR #317 + + * 2014-07-01 davidhubbard + override request() to fix "429 Too Many Requests" + + * 2014-06-30 Matías Aguirre + Tox runner with pyenv support + + * 2014-06-24 Martey Dodoo + Update link to Django example in documentation. + + * 2014-06-22 Roman Levin + Add note about access_type in docs + + * 2014-06-22 Avi Alkalay + user first_date doesn't belong here + + * 2014-06-21 Avi Alkalay + The Moves app backend + + * 2014-06-18 Matías Aguirre + Initial work towards OpenIdConnect. Refs #300. Refs #284 + + * 2014-06-18 Matías Aguirre + Useful debug pipeling function + + * 2014-06-18 Gabriel Le Breton + text should not go into code block + + * 2014-06-18 Nikolaev Andrey + It was impossible to change the version API Vkotnakte + + * 2014-06-16 Matías Aguirre + Improve django example application look + + * 2014-06-16 Matías Aguirre + Fix key access on instagram backend + + * 2014-06-14 Matías Aguirre + Integrate flask app and flask mongoengine app + + * 2014-06-14 Matías Aguirre + PEP8 + + * 2014-06-14 Matías Aguirre + Fix docstring + + * 2014-06-14 Matías Aguirre + Move common fields to base class in sqlalchemy ORMs. + + * 2014-06-14 Matías Aguirre + Use mongoengin ORM in django me app + + * 2014-06-12 Matías Aguirre + QQ backend + + * 2014-06-09 Josh Hawn + Update docker backend with Docker Hub endpoints + + * 2014-06-08 Matías Aguirre + Add missing module + + * 2014-06-08 Matías Aguirre + Set user backend reference in django app + + * 2014-06-08 Matías Aguirre + Update tests + + * 2014-06-07 Matías Aguirre + Version change (no backward compatible change) + + * 2014-05-26 Matías Aguirre + Refactor backend/strategy to avoid circular dependency + + * 2014-06-07 Matías Aguirre + Support MergeDict and MultiDict in partial cleanup. Refs #291 + +2014-06-07 v0.1.26 +================== + + * 2014-06-07 Matías Aguirre + v0.1.26 + + * 2014-06-07 Matías Aguirre + Fix google-plus scope, support server-side flow + +2014-06-07 v0.1.25 +================== + + * 2014-06-07 Matías Aguirre + v0.1.25 + + * 2014-06-07 Matías Aguirre + Support deprecated and new Google API. Refs #292. Refs #285 + + * 2014-06-07 Matías Aguirre + Fix pipeline example + + * 2014-06-01 Matías Aguirre + Document steam player data saving + + * 2014-05-28 Matías Aguirre + Remove eclipse settings from PR merge + + * 2014-05-26 Matías Aguirre + Document google scopes deprecation. Refs #285 + + * 2014-05-26 Matías Aguirre + Fix title underline + + * 2014-05-26 Matías Aguirre + Make request parameter optional. Refs #286 + + * 2014-05-26 Matías Aguirre + PEP8 + + * 2014-05-24 Devin Sevilla + Rdio API methods use POST + + * 2014-05-22 Michael Godshall + Fixed Django 1.7 admin + + * 2014-05-20 Hector Zhao + avoid updating default settings + + * 2014-05-17 Matías Aguirre + v0.2.0-dev + +2014-05-17 v0.1.24 +================== + + * 2014-05-17 Matías Aguirre + v0.1.24 + + * 2014-05-17 Matías Aguirre + Example for ajax auth. Refs #272, #238 + + * 2014-05-17 Matías Aguirre + Circumvent recursive import issue in admin. Fixes #269 + + * 2014-05-17 Matías Aguirre + Update google scopes, remove the soon to be deprecated ones. Fixes #273 + + * 2014-05-17 Matías Aguirre + Fix title underline in docs + + * 2014-05-17 Ryan Choi + remove mashery stuff from oauth; constrain it to beats + + * 2014-05-17 Ryan Choi + oauth for beats + + * 2014-05-15 Jason Sanford + Add links. + + * 2014-05-15 Ryan Choi + remove commented code for spotify + + * 2014-05-15 Ryan Choi + spotify oauth + + * 2014-05-15 Jason Sanford + Python 2.6-friendly string formatting. + + * 2014-05-15 Jason Sanford + Document MapMyFitness + + * 2014-05-15 Matías Aguirre + Change priority for new user redirect location + + * 2014-05-15 Jason Sanford + Test MapMyFitness backend + + * 2014-05-14 Jason Sanford + Get started with MapMyFitness OAuth2 + + * 2014-05-13 Matías Aguirre + MongoEngine ORM support for flask applications + + * 2014-05-13 swmerko + from http API to https API + + * 2014-05-12 Matías Aguirre + Remove unused import + + * 2014-05-10 Mark Lee + Replace references to python-oauth2 with references to requests-oauthlib + + * 2014-05-08 Smamaxs + get email on login + + * 2014-05-07 Marno Krahmer + Change the authorization url for the xing api + + * 2014-05-06 Matías Aguirre + PEP8 and docs about Facebook Graph 2.0 backends + + * 2014-05-06 Matías Aguirre + Settings to override default scope/attrs and docs about them. Refs #258 + + * 2014-05-06 Daniel Ryan + added new backend classes for Facebook that use the Open Graph 2.0 + endpoints + + * 2014-05-01 Matías Aguirre + PEP8 and logic simplification + + * 2014-05-01 Kyle Richelhoff + Added LoginRadius backend. + + * 2014-04-30 momamene + Add Kakao backend + + * 2014-04-30 Matías Aguirre + Update amazon docs, drop outdate details about bug. Fixes #260 + + * 2014-04-23 Matías Aguirre + Disable redirect_state in strava backend. Fixes #259 + + * 2014-04-23 Matías Aguirre + Allow overrideable values for AX schema attrs and SReg attributes in + OpenId. Fixes #258 + + * 2014-04-23 Matías Aguirre + Refactor fullname, first name and last name generation. Fixes #240 + + * 2014-04-23 Serg Baburin + Using https as required by the API. + + * 2014-04-19 Your Name + User model fields accessors clashes issue solved + + * 2014-04-18 Matías Aguirre + Switch VK OpenAPI to session intead of cookies. + + * 2014-04-14 David Blado + linkedin now requires redirect uris to be verified: + https://developer.linkedin.com/blog/register-your-oauth-2-redirect-urls + + * 2014-04-14 Matías Aguirre + PEP8 + + * 2014-04-14 Hannes Ljungberg + Add Twitch backend + + * 2014-04-10 Matías Aguirre + Remove unused parameters from pipeline prototypes + + * 2014-04-08 Alexander Chernigov + Handle properly refusing when entering via twitter + + * 2014-04-04 Matías Aguirre + Remove doc about deprecated setting. Refs #241 + + * 2014-04-03 Matías Aguirre + Option for open id providers to specify the username key in the values + + * 2014-04-03 Matías Aguirre + Include strava backend in the index + + * 2014-04-02 (cdep) illabout + Fix small spelling mistake. + + * 2014-04-02 Joe Hura + Add support for Vimeo OAuth 2 as part of Vimeo API v3 + + * 2014-04-01 Krishan Gupta + Update settings.rst + + * 2014-04-01 Damien + Incorrect syntax given in the documention + + * 2014-04-01 Matías Aguirre + Fix use-case snippet + + * 2014-04-01 Matías Aguirre + Switch custom redirect state to off in mendeley OAuth2. Closes #234 + + * 2014-04-01 Matías Aguirre + Enable Last.fm in example applications + + * 2014-04-01 Matías Aguirre + Last.fm docs + + * 2014-04-01 Matías Aguirre + Refactor Last.fm backend (simplify code) + + * 2014-03-26 root + Added backend for Last.Fm. There is probably an easier way to implement + this. + + * 2014-03-28 Matías Aguirre + Make exception raise optional with setting. Add tests and docs + + * 2014-03-27 Matías Aguirre + Stop tox on first error + + * 2014-03-27 Matías Aguirre + Avoid passing multiple arguments to disconnect partial pipeline + + * 2014-03-27 Matías Aguirre + Improve partial session cleaner code. Refs #231 + + * 2014-03-27 Piotr Czesław Kalmus + login with bitbucket account, error when any verified email is set + + * 2014-03-27 Matías Aguirre + Link docker docs in backends index + + * 2014-03-27 Matías Aguirre + PEP8 + + * 2014-03-25 Fernando + initial version of docker backend + + * 2014-03-26 Matías Aguirre + Flag dev version + 2014-03-26 v0.1.23 ================== diff --git a/social/__init__.py b/social/__init__.py index 27782b198..80f93b9ee 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 0) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 4eca2efe57e774258fb81d2976583fe165cb2ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 12:42:39 -0300 Subject: [PATCH 341/890] Flag dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 80f93b9ee..692a1908c 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 0) -extra = '' +version = (0, 2, 1) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From abd64d18d2290d59f331f0d4bf430b772d3e919f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 12:44:45 -0300 Subject: [PATCH 342/890] Mension product-name google requirement --- docs/backends/google.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 2806b19cb..32b1ed444 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -33,9 +33,8 @@ Recently Google launched OAuth2 support following the definition at `OAuth2 draf It works in a similar way to plain OAuth mechanism, but developers **must** register an application and apply for a set of keys. Check `Google OAuth2`_ document for details. -**Note**: - This support is experimental as Google implementation may change and OAuth2 is still - a draft. +When creating the application in the Google Console be sure to fill the +``PRODUCT NAME`` at ``API & auth -> Consent screen`` form. To enable OAuth2 support: From 9a69ad4de8f600aae00aa98fdcb18bb874578223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 13:11:59 -0300 Subject: [PATCH 343/890] Take into account inconsistent instagram responses --- social/backends/instagram.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index 881047872..509aa010f 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -12,11 +12,16 @@ class InstagramOAuth2(BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' def get_user_id(self, details, response): - return response['user']['id'] + # Sometimes Instagram returns 'user', sometimes 'data', but API docs + # says 'data' http://instagram.com/developer/endpoints/users/#get_users + user = response.get('user') or response.get('data') or {} + return user.get('id') def get_user_details(self, response): """Return user details from Instagram account""" - user = response['data'] + # Sometimes Instagram returns 'user', sometimes 'data', but API docs + # says 'data' http://instagram.com/developer/endpoints/users/#get_users + user = response.get('user') or response.get('data') or {} username = user['username'] email = user.get('email', '') fullname, first_name, last_name = self.get_user_names( From 09004a9b057364f117b03bfebf1049d26d77d3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 13:13:20 -0300 Subject: [PATCH 344/890] v0.2.1 --- Changelog | 14 +++++++++++++- social/__init__.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 3396c79c4..748655066 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,18 @@ -2014-09-11 v0.2.0 +2014-09-11 v0.2.1 ================= + * 2014-09-11 Matías Aguirre + v0.2.1 + + * 2014-09-11 Matías Aguirre + Take into account inconsistent instagram responses + + * 2014-09-11 Matías Aguirre + Mension product-name google requirement + + * 2014-09-11 Matías Aguirre + Flag dev version + * 2014-09-11 Matías Aguirre v0.2.0 diff --git a/social/__init__.py b/social/__init__.py index 692a1908c..eedeb2f59 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 1) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From a41d2f5f74cf9cd6b90c23e05b4f7f957ad3f00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Sep 2014 13:14:37 -0300 Subject: [PATCH 345/890] Flag dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index eedeb2f59..d5d5847c9 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 1) -extra = '' +version = (0, 2, 2) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From a1886b0f88bf3e936d050f6ab67a7d171994876a Mon Sep 17 00:00:00 2001 From: David Henderson Date: Fri, 12 Sep 2014 12:13:48 +0100 Subject: [PATCH 346/890] Updated to use latest api wrapper --- docs/backends/shopify.rst | 2 +- social/backends/shopify.py | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/backends/shopify.rst b/docs/backends/shopify.rst index 91913bf84..fec9f8aa4 100644 --- a/docs/backends/shopify.rst +++ b/docs/backends/shopify.rst @@ -4,7 +4,7 @@ Shopify Shopify uses OAuth 2 for authentication. To use this backend, you must install the package ``shopify`` from the `Github -project`_. +project`_. Currently supports v2+ - Register a new application at `Shopify Partners`_, and diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 61212b9b6..b5b0f4c7c 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -32,10 +32,22 @@ def shopifyAPI(self): def get_user_details(self, response): """Use the shopify store name as the username""" return { - 'username': six.text_type(response.get('shop', ''), 'utf-8') + 'username': six.text_type(response.get('shop', '')) .replace('.myshopify.com', '') } + + def extra_data(self, user, uid, response, details=None): + """Return access_token and extra defined names to store in + extra_data field""" + data = super(BaseOAuth2, self).extra_data(user, uid, response, details) + session = self.shopifyAPI.Session(self.data.get('shop').strip()) + # Get, and store the permanent token + token = session.request_token(data["access_token"].dicts[1]) + data["access_token"] = token + + return dict(data) + def auth_url(self): key, secret = self.get_key_and_secret() self.shopifyAPI.Session.setup(api_key=key, secret=secret) @@ -43,8 +55,9 @@ def auth_url(self): state = self.state_token() self.strategy.session_set(self.name + '_state', state) redirect_uri = self.get_redirect_uri(state) - return self.shopifyAPI.Session.create_permission_url( - self.data.get('shop').strip(), + session = self.shopifyAPI.Session(self.data.get('shop').strip()) + + return session.create_permission_url( scope=scope, redirect_uri=redirect_uri ) @@ -82,3 +95,4 @@ def do_auth(self, access_token, shop_url, website, *args, **kwargs): } }) return self.strategy.authenticate(*args, **kwargs) + From cd2a3c8688e56a543dd7b092e66a9ab07895cade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 12 Sep 2014 11:19:12 -0300 Subject: [PATCH 347/890] Update snippet --- docs/use_cases.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 8618e4de3..b79ec03e1 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -116,7 +116,8 @@ code follows Django conventions, but versions for others frameworks can be implemented easily):: from django.contrib.auth import login - from social.apps.django_app.utils import strategy + + from social.apps.django_app.utils import psa # Define an URL entry to point to this view, call it passing the # access_token parameter like ?access_token=. The URL entry must @@ -125,11 +126,12 @@ implemented easily):: # url(r'^register-by-token/(?P[^/]+)/$', # 'register_by_access_token') - @strategy('social:complete') + @psa('social:complete') def register_by_access_token(request, backend): - # This view expects an access_token GET parameter + # This view expects an access_token GET parameter, if it's needed, + # request.backend and request.strategy will be loaded with the current + # backend and strategy. token = request.GET.get('access_token') - backend = request.strategy.backend user = backend.do_auth(request.GET.get('access_token')) if user: login(request, user) From d507a174ecebd4e8e96c5a8d3cdbc6c977dc8254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 12 Sep 2014 11:19:23 -0300 Subject: [PATCH 348/890] Add debug pipeline to example app --- examples/django_example/example/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 9243408cf..b9c7dd1e2 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -226,6 +226,7 @@ 'social.pipeline.mail.mail_validation', 'social.pipeline.user.create_user', 'social.pipeline.social_auth.associate_user', + 'social.pipeline.debug.debug', 'social.pipeline.social_auth.load_extra_data', 'social.pipeline.user.user_details', #'social.pipeline.debug.debug' From 81179bd8755df1361d2d0fe219df26308dafa951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Kr=C3=B6ner?= Date: Tue, 16 Sep 2014 11:59:28 +0200 Subject: [PATCH 349/890] Allow more Trello settings --- docs/backends/trello.rst | 10 ++++++++++ social/backends/trello.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/docs/backends/trello.rst b/docs/backends/trello.rst index 983149b16..4b55f9931 100644 --- a/docs/backends/trello.rst +++ b/docs/backends/trello.rst @@ -12,5 +12,15 @@ In order to enable it, follow: SOCIAL_AUTH_TRELLO_KEY = '...' SOCIAL_AUTH_TRELLO_SECRET = '...' +There are also two optional settings: + +- your app name, otherwise the authorization page will say "Let An unknown application use your account?":: + + SOCIAL_AUTH_TRELLO_APP_NAME = 'My App' + +- the expiration period, social auth defaults to 'never', but you can change it:: + + SOCIAL_AUTH_TRELLO_EXPIRATION = '30days' + .. _Trello Developers API Keys: https://trello.com/1/appKey/generate diff --git a/social/backends/trello.py b/social/backends/trello.py index 73eaa916d..f07cc4530 100644 --- a/social/backends/trello.py +++ b/social/backends/trello.py @@ -38,3 +38,10 @@ def user_data(self, access_token): return self.get_json(url, auth=self.oauth_auth(access_token)) except ValueError: return None + + def auth_extra_arguments(self): + return { + 'name': self.setting('APP_NAME', ''), + # trello default expiration is '30days' + 'expiration': self.setting('EXPIRATION', 'never') + } From 6591ecb735c8f52c4e9aee24310ad89b0d4030b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 18 Sep 2014 12:01:21 -0300 Subject: [PATCH 350/890] Print arguments in the debug pipeline --- social/pipeline/debug.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/social/pipeline/debug.py b/social/pipeline/debug.py index 754dc61ec..7ef102269 100644 --- a/social/pipeline/debug.py +++ b/social/pipeline/debug.py @@ -7,3 +7,7 @@ def debug(response, details, *args, **kwargs): print('=' * 80) pprint(details) print('=' * 80) + pprint(args) + print('=' * 80) + pprint(kwargs) + print('=' * 80) From bc8dc59271b975c029af7edd7184486433a27089 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Sun, 21 Sep 2014 12:28:05 +1000 Subject: [PATCH 351/890] Update README.rst --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index f845fcb96..e0a3eee20 100644 --- a/README.rst +++ b/README.rst @@ -188,6 +188,19 @@ Or:: $ cd python-social-auth $ sudo python setup.py install + +Upgrading +--------- + +Django with South +~~~~~~~~~~~~~~~~~ + +Upgrading from 0.1 to 0.2 is likely to cause problems trying to apply a migration when the tables +already exist. In this case a fake migration needs to be applied:: + + $ python manage.py migrate --fake default + + Support --------------------- From f0dce8d4d4974426a4f1318e41970c219801a8f2 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Sun, 21 Sep 2014 12:30:29 +1000 Subject: [PATCH 352/890] Update installing.rst --- docs/installing.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/installing.rst b/docs/installing.rst index 09cb1d6bf..f0a132a3d 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -46,3 +46,13 @@ Or:: .. _python-openid: http://pypi.python.org/pypi/python-openid/ .. _requests-oauthlib: https://requests-oauthlib.readthedocs.org/ .. _sqlalchemy: http://www.sqlalchemy.org/ + +Upgrading +--------- + +Django with South +~~~~~~~~~~~~~~~~~ + +Upgrading from 0.1 to 0.2 is likely to cause problems trying to apply a migration when the tables already exist. In this case a fake migration needs to be applied: + +$ python manage.py migrate --fake default From f138221b12df275a4ddcece83f212998c30428dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 21 Sep 2014 17:04:52 -0300 Subject: [PATCH 353/890] Don't update a setting value. Refs #378. Refs #377 --- social/backends/google.py | 2 +- social/backends/oauth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/google.py b/social/backends/google.py index ef5868e42..e1c050583 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -60,7 +60,7 @@ def get_scope(self): default_scope = self.DEPRECATED_DEFAULT_SCOPE else: default_scope = self.DEFAULT_SCOPE - scope += default_scope or [] + scope = scope + (default_scope or []) return scope def user_data(self, access_token, *args, **kwargs): diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 21b3d201b..169643975 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -95,7 +95,7 @@ def get_scope(self): """Return list with needed access scope""" scope = self.setting('SCOPE', []) if not self.setting('IGNORE_DEFAULT_SCOPE', False): - scope += self.DEFAULT_SCOPE or [] + scope = scope + (self.DEFAULT_SCOPE or []) return scope def get_scope_argument(self): From 7b413e85fdfe78976a36190cb2db324664479fa5 Mon Sep 17 00:00:00 2001 From: David Henderson Date: Tue, 23 Sep 2014 09:27:38 +0100 Subject: [PATCH 354/890] No good reason to skip a class --- social/backends/shopify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/shopify.py b/social/backends/shopify.py index b5b0f4c7c..44e5979d4 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -40,7 +40,7 @@ def get_user_details(self, response): def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in extra_data field""" - data = super(BaseOAuth2, self).extra_data(user, uid, response, details) + data = super(ShopifyOAuth2, self).extra_data(user, uid, response, details) session = self.shopifyAPI.Session(self.data.get('shop').strip()) # Get, and store the permanent token token = session.request_token(data["access_token"].dicts[1]) From fd9b8dd2dc8531d877ab04fb022c3fac32dd60ac Mon Sep 17 00:00:00 2001 From: David Henderson Date: Tue, 23 Sep 2014 09:48:59 +0100 Subject: [PATCH 355/890] Removed prefix, added example of details object. --- social/backends/exacttarget.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/social/backends/exacttarget.py b/social/backends/exacttarget.py index 6da9f7498..bc5ef422c 100644 --- a/social/backends/exacttarget.py +++ b/social/backends/exacttarget.py @@ -25,8 +25,21 @@ def get_user_details(self, response): def get_user_id(self, details, response): - """Create a user ID from the ET user ID. Uses details rather than the default response""" - return "exacttarget_%s" % details.get('id') + """Create a user ID from the ET user ID. Uses details rather than the default response, + as only the token is available in response. details is much richer: + { + 'expiresIn': 1200, + 'username': 'example@example.com', + 'refreshToken': '1234567890abcdef', + 'internalOauthToken': 'jwttoken.......', + 'oauthToken': 'yetanothertoken', + 'id': 123456, + 'culture': 'en-US', + 'timezone': {'shortName': 'CST', 'offset': -6.0, 'dst': False, 'longName': '(GMT-06:00) Central Time (No Daylight Saving)'}, + 'email': 'example@example.com' + } + """ + return "%s" % details.get('id') def uses_redirect(self): return False From 2f7c74c257ce976f70f4e75c5fb612ad383f7e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 23 Sep 2014 11:17:26 -0300 Subject: [PATCH 356/890] PEP8 --- social/backends/exacttarget.py | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/social/backends/exacttarget.py b/social/backends/exacttarget.py index bc5ef422c..05892971a 100644 --- a/social/backends/exacttarget.py +++ b/social/backends/exacttarget.py @@ -23,23 +23,29 @@ def get_user_details(self, response): user['username'] = user['email'] return user - def get_user_id(self, details, response): - """Create a user ID from the ET user ID. Uses details rather than the default response, - as only the token is available in response. details is much richer: - { - 'expiresIn': 1200, - 'username': 'example@example.com', - 'refreshToken': '1234567890abcdef', - 'internalOauthToken': 'jwttoken.......', - 'oauthToken': 'yetanothertoken', - 'id': 123456, - 'culture': 'en-US', - 'timezone': {'shortName': 'CST', 'offset': -6.0, 'dst': False, 'longName': '(GMT-06:00) Central Time (No Daylight Saving)'}, - 'email': 'example@example.com' - } """ - return "%s" % details.get('id') + Create a user ID from the ET user ID. Uses details rather than the + default response, as only the token is available in response. details + is much richer: + { + 'expiresIn': 1200, + 'username': 'example@example.com', + 'refreshToken': '1234567890abcdef', + 'internalOauthToken': 'jwttoken.......', + 'oauthToken': 'yetanothertoken', + 'id': 123456, + 'culture': 'en-US', + 'timezone': { + 'shortName': 'CST', + 'offset': -6.0, + 'dst': False, + 'longName': '(GMT-06:00) Central Time (No Daylight Saving)' + }, + 'email': 'example@example.com' + } + """ + return '{0}'.format(details.get('id')) def uses_redirect(self): return False From ab9f51e658160cabb8fb06fd9f2260fc4fefa455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 23 Sep 2014 11:29:02 -0300 Subject: [PATCH 357/890] PEP8 --- social/backends/shopify.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 44e5979d4..407856769 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -32,20 +32,20 @@ def shopifyAPI(self): def get_user_details(self, response): """Use the shopify store name as the username""" return { - 'username': six.text_type(response.get('shop', '')) - .replace('.myshopify.com', '') + 'username': six.text_type(response.get('shop', '')).replace( + '.myshopify.com', '' + ) } - def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in extra_data field""" - data = super(ShopifyOAuth2, self).extra_data(user, uid, response, details) + data = super(ShopifyOAuth2, self).extra_data(user, uid, response, + details) session = self.shopifyAPI.Session(self.data.get('shop').strip()) # Get, and store the permanent token - token = session.request_token(data["access_token"].dicts[1]) - data["access_token"] = token - + token = session.request_token(data['access_token'].dicts[1]) + data['access_token'] = token return dict(data) def auth_url(self): @@ -56,7 +56,6 @@ def auth_url(self): self.strategy.session_set(self.name + '_state', state) redirect_uri = self.get_redirect_uri(state) session = self.shopifyAPI.Session(self.data.get('shop').strip()) - return session.create_permission_url( scope=scope, redirect_uri=redirect_uri @@ -95,4 +94,3 @@ def do_auth(self, access_token, shop_url, website, *args, **kwargs): } }) return self.strategy.authenticate(*args, **kwargs) - From df6d8fe3f44433274e85c07b883e9d5e3468da84 Mon Sep 17 00:00:00 2001 From: dzerrenner Date: Wed, 24 Sep 2014 18:08:06 +0200 Subject: [PATCH 358/890] added a backend for Battle.net Oauth2 auth --- docs/backends/battlenet.rst | 30 ++++++++++++++++++++++ docs/use_cases.rst | 48 ++++++++++++++++++++++++++++++++++++ social/backends/battlenet.py | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 docs/backends/battlenet.rst create mode 100644 social/backends/battlenet.py diff --git a/docs/backends/battlenet.rst b/docs/backends/battlenet.rst new file mode 100644 index 000000000..ba2bc0c08 --- /dev/null +++ b/docs/backends/battlenet.rst @@ -0,0 +1,30 @@ +Battle.net +========== + +Blizzard implemented OAuth2 protocol for their authentication mechanism. To +enable ``python-social-auth`` support follow this steps: + +1. Go to `Battlenet Developer Portal`_ and create an application. + +2. Fill App Id and Secret in your project settings:: + + SOCIAL_AUTH_BATTLENET_OAUTH2_KEY = '...' + SOCIAL_AUTH_BATTLENET_OAUTH2_SECRET = '...' + +3. Enable the backend:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.battlenet.BattleNetOAuth2', + ... + ) + +Note: The API returns an accountId which will be used as identifier for the user. +If you want to allow the user to choose a username from his own characters, some +further steps are required, see the use cases part of the documentation. + +Further documentation at `Developer Guide`_. + +.. _Battlenet Developer Portal: https://dev.battle.net/ +.. _Developer Guide: https://dev.battle.net/docs/read/oauth + diff --git a/docs/use_cases.rst b/docs/use_cases.rst index b79ec03e1..ee6ca7e90 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -209,3 +209,51 @@ accomplish that behavior, there are two ways to do it. .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list + + +Login to battle.net and enable a user to choose a username from his World of Warcraft characters +------------------------------------------------------------------------------------------------ + +If you want to register new users on your site via battle.net, you can enable these users to +choose a username from their own World-of-Warcraft characters. To do this, use the +``battlenet-oauth2`` backend along with a small form to choose the username. + +The form is rendered +via a partial pipeline item like this:: + + @partial + def pick_character_name(backend, details, response, is_new=False, *args, **kwargs): + if backend.name == 'battlenet-oauth2' and is_new: + data = backend.strategy.request_data() + if data.get('character_name') is None: + # New user and didn't pick a character name yet, so we render + # and send a form to pick one. The form must do a POST/GET + # request to the same URL (/complete/battlenet-oauth2/). In this + # example we expect the user option under the key: + # character_name + # you have to filter the result list according to your needs. In this + # case, only guild members are allowed to sign up. + + char_list = [c['name'] for c in backend.get_characters(response.get('access_token')) + if 'guild' in c and c['guild'] == ''] # ToDo: make guild name a parameter + return render_to_response('pick_character_form.html', {'charlist': char_list, }) + else: + # The user selected a character name + return {'username': data.get('character_name')} + +after that, add your partial to the pipeline:: + + SOCIAL_AUTH_PIPELINE = ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.auth_allowed', + 'social.pipeline.social_auth.social_user', + 'social.pipeline.user.get_username', + 'path.to.pick_character_name', + 'social.pipeline.user.create_user', + 'social.pipeline.social_auth.associate_user', + 'social.pipeline.social_auth.load_extra_data', + 'social.pipeline.user.user_details', + ) + +It needs to be somewhere before create_user because the partial will change the username according to the users choice. \ No newline at end of file diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py new file mode 100644 index 000000000..f98a553b3 --- /dev/null +++ b/social/backends/battlenet.py @@ -0,0 +1,46 @@ +from social.backends.oauth import BaseOAuth2 + +class BattleNetOAuth2(BaseOAuth2): + """ battle.net Oauth2 backend""" + name = 'battlenet-oauth2' + REDIRECT_STATE = False + AUTHORIZATION_URL = 'https://eu.battle.net/oauth/authorize' + ACCESS_TOKEN_URL = 'https://eu.battle.net/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + # REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' + REVOKE_TOKEN_METHOD = 'GET' + DEFAULT_SCOPE = ['wow.profile', ] + ID_KEY = 'accountId' + + EXTRA_DATA = [ + ('refresh_token', 'refresh_token', True), + ('expires_in', 'expires'), + ('token_type', 'token_type', True) + ] + + def get_characters(self, access_token): + """ + fetches the character list from the battle.net API. + :param access_token: the access token for the user which character list is fetched + :return: list of characters or empty list if the request fails. + """ + params = {'access_token': access_token} + if self.setting('API_LOCALE'): + params['locale'] = self.setting('API_LOCALE') + response = self.get_json('https://eu.api.battle.net/wow/user/characters', params=params) + return response.get('characters') or [] + + def get_user_details(self, response): + """ Return user details from Battle.net account """ + return { + 'battletag': response.get('battletag') + } + + def user_data(self, access_token, *args, **kwargs): + """ Loads user data from service """ + user_data = self.get_json( + 'https://eu.api.battle.net/account/user/battletag', + params={'access_token': access_token} + ) + print("user_data:", user_data) + return user_data \ No newline at end of file From 93d3aa514aef97637e28d31ea14532332dbb869c Mon Sep 17 00:00:00 2001 From: Vera Mazhuga Date: Wed, 24 Sep 2014 15:45:18 -0500 Subject: [PATCH 359/890] master add SCOPE_SEPARATOR to DisqusOAuth2 --- social/backends/disqus.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/disqus.py b/social/backends/disqus.py index df516b1cc..c36be28d8 100644 --- a/social/backends/disqus.py +++ b/social/backends/disqus.py @@ -10,6 +10,7 @@ class DisqusOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://disqus.com/api/oauth/2.0/authorize/' ACCESS_TOKEN_URL = 'https://disqus.com/api/oauth/2.0/access_token/' ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' EXTRA_DATA = [ ('avatar', 'avatar'), ('connections', 'connections'), From da0215be1fce6015ad9ade186e97645763d9b8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 25 Sep 2014 14:18:52 -0300 Subject: [PATCH 360/890] PEP8 and more --- docs/backends/battlenet.rst | 8 ++++---- docs/use_cases.rst | 37 +++++++++++++++++++----------------- social/backends/battlenet.py | 26 ++++++++++++------------- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/docs/backends/battlenet.rst b/docs/backends/battlenet.rst index ba2bc0c08..65a1ac08b 100644 --- a/docs/backends/battlenet.rst +++ b/docs/backends/battlenet.rst @@ -19,12 +19,12 @@ enable ``python-social-auth`` support follow this steps: ... ) -Note: The API returns an accountId which will be used as identifier for the user. -If you want to allow the user to choose a username from his own characters, some -further steps are required, see the use cases part of the documentation. +Note: The API returns an accountId which will be used as identifier for the +user. If you want to allow the user to choose a username from his own +characters, some further steps are required, see the use cases part of the +documentation. Further documentation at `Developer Guide`_. .. _Battlenet Developer Portal: https://dev.battle.net/ .. _Developer Guide: https://dev.battle.net/docs/read/oauth - diff --git a/docs/use_cases.rst b/docs/use_cases.rst index ee6ca7e90..f7c2ec8b7 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -207,19 +207,16 @@ accomplish that behavior, there are two ways to do it. backend with ``user.social_auth.get(provider='facebook-custom')`` and use the ``access_token`` in it. -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _People API endpoint: https://developers.google.com/+/api/latest/people/list +Enable a user to choose a username from his World of Warcraft characters +------------------------------------------------------------------------ -Login to battle.net and enable a user to choose a username from his World of Warcraft characters ------------------------------------------------------------------------------------------------- +If you want to register new users on your site via battle.net, you can enable +these users to choose a username from their own World-of-Warcraft characters. +To do this, use the ``battlenet-oauth2`` backend along with a small form to +choose the username. -If you want to register new users on your site via battle.net, you can enable these users to -choose a username from their own World-of-Warcraft characters. To do this, use the -``battlenet-oauth2`` backend along with a small form to choose the username. - -The form is rendered -via a partial pipeline item like this:: +The form is rendered via a partial pipeline item like this:: @partial def pick_character_name(backend, details, response, is_new=False, *args, **kwargs): @@ -231,17 +228,18 @@ via a partial pipeline item like this:: # request to the same URL (/complete/battlenet-oauth2/). In this # example we expect the user option under the key: # character_name - # you have to filter the result list according to your needs. In this - # case, only guild members are allowed to sign up. - - char_list = [c['name'] for c in backend.get_characters(response.get('access_token')) - if 'guild' in c and c['guild'] == ''] # ToDo: make guild name a parameter + # you have to filter the result list according to your needs. + # In this example, only guild members are allowed to sign up. + char_list = [ + c['name'] for c in backend.get_characters(response.get('access_token')) + if 'guild' in c and c['guild'] == '' + ] return render_to_response('pick_character_form.html', {'charlist': char_list, }) else: # The user selected a character name return {'username': data.get('character_name')} -after that, add your partial to the pipeline:: +Don't forget to add the partial to the pipeline:: SOCIAL_AUTH_PIPELINE = ( 'social.pipeline.social_auth.social_details', @@ -256,4 +254,9 @@ after that, add your partial to the pipeline:: 'social.pipeline.user.user_details', ) -It needs to be somewhere before create_user because the partial will change the username according to the users choice. \ No newline at end of file +It needs to be somewhere before create_user because the partial will change the +username according to the users choice. + + +.. _python-social-auth: https://github.com/omab/python-social-auth +.. _People API endpoint: https://developers.google.com/+/api/latest/people/list diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py index f98a553b3..1cb5a9152 100644 --- a/social/backends/battlenet.py +++ b/social/backends/battlenet.py @@ -1,17 +1,16 @@ from social.backends.oauth import BaseOAuth2 + class BattleNetOAuth2(BaseOAuth2): """ battle.net Oauth2 backend""" name = 'battlenet-oauth2' + ID_KEY = 'accountId' REDIRECT_STATE = False AUTHORIZATION_URL = 'https://eu.battle.net/oauth/authorize' ACCESS_TOKEN_URL = 'https://eu.battle.net/oauth/token' ACCESS_TOKEN_METHOD = 'POST' - # REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['wow.profile', ] - ID_KEY = 'accountId' - + DEFAULT_SCOPE = ['wow.profile'] EXTRA_DATA = [ ('refresh_token', 'refresh_token', True), ('expires_in', 'expires'), @@ -20,27 +19,26 @@ class BattleNetOAuth2(BaseOAuth2): def get_characters(self, access_token): """ - fetches the character list from the battle.net API. - :param access_token: the access token for the user which character list is fetched - :return: list of characters or empty list if the request fails. + Fetches the character list from the battle.net API. Returns list of + characters or empty list if the request fails. """ params = {'access_token': access_token} if self.setting('API_LOCALE'): params['locale'] = self.setting('API_LOCALE') - response = self.get_json('https://eu.api.battle.net/wow/user/characters', params=params) + + response = self.get_json( + 'https://eu.api.battle.net/wow/user/characters', + params=params + ) return response.get('characters') or [] def get_user_details(self, response): """ Return user details from Battle.net account """ - return { - 'battletag': response.get('battletag') - } + return {'battletag': response.get('battletag')} def user_data(self, access_token, *args, **kwargs): """ Loads user data from service """ - user_data = self.get_json( + return self.get_json( 'https://eu.api.battle.net/account/user/battletag', params={'access_token': access_token} ) - print("user_data:", user_data) - return user_data \ No newline at end of file From 94ba30f998e747871eb8cad9eb137c7ae856f229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 25 Sep 2014 14:27:45 -0300 Subject: [PATCH 361/890] Configurable django views namespace. Refs #399 --- social/apps/django_app/views.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 0e506c7dc..9123190fa 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -1,19 +1,24 @@ +from django.conf import settings from django.contrib.auth import login, REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt, csrf_protect from django.views.decorators.http import require_POST +from social.utils import setting_name from social.actions import do_auth, do_complete, do_disconnect from social.apps.django_app.utils import psa -@psa('social:complete') +NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'social' + + +@psa('{0}:complete'.format(NAMESPACE)) def auth(request, backend): return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME) @csrf_exempt -@psa('social:complete') +@psa('{0}:complete'.format(NAMESPACE)) def complete(request, backend, *args, **kwargs): """Authentication complete view""" return do_complete(request.backend, _do_login, request.user, From 442b068323322515e5e903378741f641b82dac28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 25 Sep 2014 14:30:52 -0300 Subject: [PATCH 362/890] Doc about custom url namespace. Refs #399 --- docs/configuration/django.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index ea1975042..8e409795d 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -82,6 +82,10 @@ Add URLs entries:: ... ) +In case you need a custom namespace, this setting is also needed:: + + SOCIAL_AUTH_URL_NAMESPACE = 'social' + Template Context Processors --------------------------- From 526439ac66b00ce801e4fc5df89633a66eb8ffb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 26 Sep 2014 16:42:36 -0300 Subject: [PATCH 363/890] Use getattr to get current backend from request --- social/apps/django_app/middleware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index c6cd2852c..1d099508c 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -27,7 +27,9 @@ def process_exception(self, request, exception): return if isinstance(exception, SocialAuthBaseException): - backend_name = request.backend.name + backend = getattr(request, 'backend', None) + backend_name = getattr(backend, 'name', 'unknown-backend') + message = self.get_message(request, exception) url = self.get_redirect_uri(request, exception) try: From 587da255680d460b60e651b351ab23e3928ba999 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 27 Sep 2014 20:58:35 +0300 Subject: [PATCH 364/890] Recreate migration with Django 1.7 final and re-PEP8. This seems to work around a false-positive migration being created even if nothing has actually changed. A similar problem occurred in easy-thumbnails: https://github.com/SmileyChris/easy-thumbnails/commit/b95e6888cd7bea9c5138b72bdbcd1e743655580f --- .../default/migrations/0001_initial.py | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 952feb09d..f28dd607c 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -2,13 +2,13 @@ from __future__ import unicode_literals from django.db import models, migrations +import social.apps.django_app.default.fields from django.conf import settings - import social.storage.django_orm -import social.apps.django_app.default.fields class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -17,8 +17,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Association', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, - auto_created=True, verbose_name='ID')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('server_url', models.CharField(max_length=255)), ('handle', models.CharField(max_length=255)), ('secret', models.CharField(max_length=255)), @@ -30,17 +30,15 @@ class Migration(migrations.Migration): 'db_table': 'social_auth_association', }, bases=( - models.Model, - social.storage.django_orm.DjangoAssociationMixin - ), + models.Model, social.storage.django_orm.DjangoAssociationMixin), ), migrations.CreateModel( name='Code', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, - auto_created=True, verbose_name='ID')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('email', models.EmailField(max_length=75)), - ('code', models.CharField(db_index=True, max_length=32)), + ('code', models.CharField(max_length=32, db_index=True)), ('verified', models.BooleanField(default=False)), ], options={ @@ -48,15 +46,11 @@ class Migration(migrations.Migration): }, bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), ), - migrations.AlterUniqueTogether( - name='code', - unique_together=set([('email', 'code')]), - ), migrations.CreateModel( name='Nonce', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, - auto_created=True, verbose_name='ID')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('server_url', models.CharField(max_length=255)), ('timestamp', models.IntegerField()), ('salt', models.CharField(max_length=65)), @@ -69,14 +63,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserSocialAuth', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, - auto_created=True, verbose_name='ID')), + ('id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('provider', models.CharField(max_length=32)), ('uid', models.CharField(max_length=255)), - ('extra_data', - social.apps.django_app.default.fields.JSONField(default='{}') - ), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('extra_data', social.apps.django_app.default.fields.JSONField( + default=b'{}')), + ('user', models.ForeignKey( + related_name=b'social_auth', to=settings.AUTH_USER_MODEL)), ], options={ 'db_table': 'social_auth_usersocialauth', @@ -87,4 +81,8 @@ class Migration(migrations.Migration): name='usersocialauth', unique_together=set([('provider', 'uid')]), ), + migrations.AlterUniqueTogether( + name='code', + unique_together=set([('email', 'code')]), + ), ] From ef14e0c7c8e2c2ddfc162cb78b615cbb39d13c28 Mon Sep 17 00:00:00 2001 From: David Zerrenner Date: Sun, 28 Sep 2014 13:03:11 +0200 Subject: [PATCH 365/890] Added some legal stuff Blizzard developers asked to add some legal information, so that no one confuses this wit official Blizzard stuff. --- social/backends/battlenet.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py index 1cb5a9152..db7bbca62 100644 --- a/social/backends/battlenet.py +++ b/social/backends/battlenet.py @@ -1,5 +1,12 @@ from social.backends.oauth import BaseOAuth2 +""" +This provides a backend for python-social-auth. This should not be confused +with officially battle.net offerings. This piece of code is not officially +affiliated with Blizzard Entertainment, copyrights to their respective owners. + +see: http://us.battle.net/en/forum/topic/13979588015 +""" class BattleNetOAuth2(BaseOAuth2): """ battle.net Oauth2 backend""" From 52cb977c4f64100e30c071c377953640872466d5 Mon Sep 17 00:00:00 2001 From: Lee Jaeyoung Date: Mon, 29 Sep 2014 16:30:31 +0900 Subject: [PATCH 366/890] Add Kakao to README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index e0a3eee20..fadf0cb5f 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,7 @@ or current ones extended): * Google_ OAuth1, OAuth2 and OpenId * Instagram_ OAuth2 * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication + * Kakao_ OAuth2 https://developer.kakao.com * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId From 2e3f0627cd1d88b79aa4ed371dc67a395ee6a0dd Mon Sep 17 00:00:00 2001 From: Lee Jaeyoung Date: Mon, 29 Sep 2014 16:31:31 +0900 Subject: [PATCH 367/890] Apply more detailed address for kakao --- docs/backends/kakao.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/kakao.rst b/docs/backends/kakao.rst index 8d78ff141..4a2fbaa16 100644 --- a/docs/backends/kakao.rst +++ b/docs/backends/kakao.rst @@ -14,4 +14,4 @@ Kakao uses OAuth v2 for Authentication. SOCIAL_AUTH_KAKAO_SCOPE = [...] -.. _Kakao API: https://developers.kakao.com/ +.. _Kakao API: https://developers.kakao.com/docs/restapi From d2dc3fff4f0b1bc8452d58fb4e79b6b2c2b2b2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 30 Sep 2014 11:00:47 -0300 Subject: [PATCH 368/890] Convert docstring to comments, PEP8 --- social/backends/battlenet.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py index db7bbca62..ecf77a1bb 100644 --- a/social/backends/battlenet.py +++ b/social/backends/battlenet.py @@ -1,12 +1,11 @@ from social.backends.oauth import BaseOAuth2 -""" -This provides a backend for python-social-auth. This should not be confused -with officially battle.net offerings. This piece of code is not officially -affiliated with Blizzard Entertainment, copyrights to their respective owners. -see: http://us.battle.net/en/forum/topic/13979588015 -""" +# This provides a backend for python-social-auth. This should not be confused +# with officially battle.net offerings. This piece of code is not officially +# affiliated with Blizzard Entertainment, copyrights to their respective +# owners. See http://us.battle.net/en/forum/topic/13979588015 for more details. + class BattleNetOAuth2(BaseOAuth2): """ battle.net Oauth2 backend""" From cc5d75dac91a4520d52973580afe17829b8b9a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 30 Sep 2014 11:07:52 -0300 Subject: [PATCH 369/890] PEP8 --- .../django_app/default/migrations/0001_initial.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index f28dd607c..42c9bb45b 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -18,7 +18,8 @@ class Migration(migrations.Migration): name='Association', fields=[ ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), ('server_url', models.CharField(max_length=255)), ('handle', models.CharField(max_length=255)), ('secret', models.CharField(max_length=255)), @@ -30,13 +31,15 @@ class Migration(migrations.Migration): 'db_table': 'social_auth_association', }, bases=( - models.Model, social.storage.django_orm.DjangoAssociationMixin), + models.Model, social.storage.django_orm.DjangoAssociationMixin + ), ), migrations.CreateModel( name='Code', fields=[ ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), ('email', models.EmailField(max_length=75)), ('code', models.CharField(max_length=32, db_index=True)), ('verified', models.BooleanField(default=False)), @@ -50,7 +53,8 @@ class Migration(migrations.Migration): name='Nonce', fields=[ ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), ('server_url', models.CharField(max_length=255)), ('timestamp', models.IntegerField()), ('salt', models.CharField(max_length=65)), @@ -64,7 +68,8 @@ class Migration(migrations.Migration): name='UserSocialAuth', fields=[ ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), ('provider', models.CharField(max_length=32)), ('uid', models.CharField(max_length=255)), ('extra_data', social.apps.django_app.default.fields.JSONField( From 68e43bec17092c4d11456872ccb5dea559327760 Mon Sep 17 00:00:00 2001 From: Laban Date: Wed, 1 Oct 2014 23:33:42 +0300 Subject: [PATCH 370/890] Incorrect import path for db model --- examples/flask_example/manage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flask_example/manage.py b/examples/flask_example/manage.py index 9953c5aa6..9c6302fcd 100755 --- a/examples/flask_example/manage.py +++ b/examples/flask_example/manage.py @@ -19,7 +19,7 @@ @manager.command def syncdb(): from flask_example.models import user - from social.apps.flask_app import models + from social.apps.flask_app.default import models db.drop_all() db.create_all() From 7f64dad08efbecaa66efb6e6d07d5741fb6b5e83 Mon Sep 17 00:00:00 2001 From: Daniel Holmes Date: Fri, 3 Oct 2014 01:08:31 +1000 Subject: [PATCH 371/890] Use new GoogleOAuth2 Spec As per #406 we've added in the openid to the GoogleOAuth2 class as per the documentation located [here](https://developers.google.com/accounts/docs/OAuth2Login). It's important to note here that openid must be first in the list of scopes as per the documentation. @avances123 this is the pull request I referenced in IRC --- social/backends/google.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/google.py b/social/backends/google.py index e1c050583..e16ad04b9 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -84,7 +84,8 @@ class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): ACCESS_TOKEN_METHOD = 'POST' REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['email', 'profile'] + # The order of the default scope is important + DEFAULT_SCOPE = ['openid', 'email', 'profile'] DEPRECATED_DEFAULT_SCOPE = [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile' From 25e4db03cebea70a84038e729dce084c58359f1d Mon Sep 17 00:00:00 2001 From: micahhausler Date: Thu, 2 Oct 2014 15:06:00 -0400 Subject: [PATCH 372/890] Added string method to UserSocialAuth model --- social/apps/django_app/default/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index cf886b976..600e9f197 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -33,6 +33,9 @@ class UserSocialAuth(models.Model, DjangoUserMixin): uid = models.CharField(max_length=UID_LENGTH) extra_data = JSONField() + def __str__(self): + return str(self.user) + class Meta: """Meta data""" unique_together = ('provider', 'uid') From 95b2a5168d4693f22997b499b37fe08e7df6bc64 Mon Sep 17 00:00:00 2001 From: micahhausler Date: Thu, 2 Oct 2014 15:06:57 -0400 Subject: [PATCH 373/890] Switched list_display order for UserSocialAuth There is typically more clickable surface area on a username than a numeric id. --- social/apps/django_app/default/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index 9b6192d14..de7802bf9 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -9,7 +9,7 @@ class UserSocialAuthOption(admin.ModelAdmin): """Social Auth user options""" - list_display = ('id', 'user', 'provider', 'uid') + list_display = ('user', 'id', 'provider', 'uid') list_filter = ('provider',) raw_id_fields = ('user',) list_select_related = True From 6ecec42abc707175b7a3f29188f002b976794808 Mon Sep 17 00:00:00 2001 From: micahhausler Date: Sat, 4 Oct 2014 10:45:53 -0400 Subject: [PATCH 374/890] Added Django 1.7 App Config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Renames the app to ‘Python Social Auth’ in the django admin --- social/apps/django_app/default/__init__.py | 2 ++ social/apps/django_app/default/apps.py | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 social/apps/django_app/default/apps.py diff --git a/social/apps/django_app/default/__init__.py b/social/apps/django_app/default/__init__.py index 6408aec79..6f0c46adc 100644 --- a/social/apps/django_app/default/__init__.py +++ b/social/apps/django_app/default/__init__.py @@ -5,3 +5,5 @@ * Add 'social.apps.django_app.default' to INSTALLED_APPS * In urls.py include url('', include('social.apps.django_app.urls')) """ + +default_app_config = 'social.apps.django_app.default.apps.PythonSocialAuthConfig' diff --git a/social/apps/django_app/default/apps.py b/social/apps/django_app/default/apps.py new file mode 100644 index 000000000..977cd3c6e --- /dev/null +++ b/social/apps/django_app/default/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PythonSocialAuthConfig(AppConfig): + name = 'social.apps.django_app.default' + verbose_name = 'Python Social Auth' From 5064481d1e75f87bde6deb88b2dc5acf301ec7b5 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 7 Oct 2014 00:21:49 +0300 Subject: [PATCH 375/890] Added Python 3.4 and PyPy to the build matrix. Also use travis_retry instead of --use-mirrors in order to prevent build failures due to network issues because --use-mirrors is deprecated. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 44443f9d8..bfab16118 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,10 @@ python: - "2.6" - "2.7" - "3.3" + - "3.4" + - "pypy" install: - "python setup.py -q install" - - "pip install -r social/tests/requirements.txt --use-mirrors" + - "travis_retry pip install -r social/tests/requirements.txt" script: - "nosetests --with-coverage --cover-package=social --where=social/tests" From 434986f30338f8287913b9392ab581bf838929b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Oct 2014 12:48:33 -0200 Subject: [PATCH 376/890] Enable pypy and replace sugar with unittest2. Refs #410 --- social/tests/actions/actions.py | 29 ++--- social/tests/actions/test_associate.py | 22 ++-- social/tests/actions/test_disconnect.py | 14 +-- social/tests/actions/test_login.py | 14 +-- social/tests/backends/base.py | 43 ++++--- social/tests/backends/legacy.py | 3 +- social/tests/backends/oauth.py | 5 +- social/tests/backends/open_id.py | 5 +- social/tests/backends/test_bitbucket.py | 9 +- social/tests/backends/test_box.py | 7 +- social/tests/backends/test_broken.py | 30 ++--- social/tests/backends/test_dummy.py | 11 +- social/tests/backends/test_evernote.py | 12 +- social/tests/backends/test_facebook.py | 8 +- social/tests/backends/test_github.py | 13 +- social/tests/backends/test_livejournal.py | 20 ++- social/tests/backends/test_reddit.py | 4 +- social/tests/backends/test_steam.py | 40 +++--- social/tests/backends/test_utils.py | 22 ++-- social/tests/requirements-python3.txt | 7 ++ social/tests/requirements.txt | 2 +- social/tests/test_exceptions.py | 5 +- social/tests/test_pipeline.py | 23 ++-- social/tests/test_storage.py | 143 +++++++++------------- social/tests/test_utils.py | 86 ++++++------- tox.ini | 8 +- 26 files changed, 280 insertions(+), 305 deletions(-) create mode 100644 social/tests/requirements-python3.txt diff --git a/social/tests/actions/actions.py b/social/tests/actions/actions.py index 9d81dc83b..3b9f42f40 100644 --- a/social/tests/actions/actions.py +++ b/social/tests/actions/actions.py @@ -1,8 +1,7 @@ import json import requests -import unittest +import unittest2 as unittest -from sure import expect from httpretty import HTTPretty from social.utils import parse_qs, module_member @@ -107,8 +106,8 @@ def do_login(self, after_complete_checks=True, user_data_body=None, body='foobar') response = requests.get(start_url) - expect(response.url).to.equal(location_url) - expect(response.text).to.equal('foobar') + self.assertEqual(response.url, location_url) + self.assertEqual(response.text, 'foobar') HTTPretty.register_uri(HTTPretty.POST, uri=self.backend.ACCESS_TOKEN_URL, @@ -129,10 +128,9 @@ def _login(backend, user, social_user): redirect = do_complete(self.backend, user=self.user, login=_login) if after_complete_checks: - expect(self.strategy.session_get('username')).to.equal( - expected_username or self.expected_username - ) - expect(redirect.url).to.equal(self.login_redirect_url) + self.assertEqual(self.strategy.session_get('username'), + expected_username or self.expected_username) + self.assertEqual(redirect.url, self.login_redirect_url) return redirect def do_login_with_partial_pipeline(self, before_complete=None): @@ -174,8 +172,8 @@ def do_login_with_partial_pipeline(self, before_complete=None): body='foobar') response = requests.get(start_url) - expect(response.url).to.equal(location_url) - expect(response.text).to.equal('foobar') + self.assertEqual(response.url, location_url) + self.assertEqual(response.text, 'foobar') HTTPretty.register_uri(HTTPretty.GET, uri=self.backend.ACCESS_TOKEN_URL, @@ -194,7 +192,7 @@ def _login(backend, user, social_user): redirect = do_complete(self.backend, user=self.user, login=_login) url = self.strategy.build_absolute_uri('/password') - expect(redirect.url).to.equal(url) + self.assertEqual(redirect.url, url) HTTPretty.register_uri(HTTPretty.GET, redirect.url, status=200, body='foobar') HTTPretty.register_uri(HTTPretty.POST, redirect.url, status=200) @@ -203,13 +201,12 @@ def _login(backend, user, social_user): requests.get(url) requests.post(url, data={'password': password}) data = parse_qs(HTTPretty.last_request.body) - expect(data['password']).to.equal(password) + self.assertEqual(data['password'], password) self.strategy.session_set('password', data['password']) if before_complete: before_complete() redirect = do_complete(self.backend, user=self.user, login=_login) - expect(self.strategy.session_get('username')).to.equal( - self.expected_username - ) - expect(redirect.url).to.equal(self.login_redirect_url) + self.assertEqual(self.strategy.session_get('username'), + self.expected_username) + self.assertEqual(redirect.url, self.login_redirect_url) diff --git a/social/tests/actions/test_associate.py b/social/tests/actions/test_associate.py index 1f2c342c1..6c9250811 100644 --- a/social/tests/actions/test_associate.py +++ b/social/tests/actions/test_associate.py @@ -1,5 +1,4 @@ import json -from sure import expect from social.exceptions import AuthAlreadyAssociated @@ -17,13 +16,13 @@ def setUp(self): def test_associate(self): self.do_login() - expect(len(self.user.social)).to.equal(1) - expect(self.user.social[0].provider).to.equal('github') + self.assertTrue(len(self.user.social), 1) + self.assertEqual(self.user.social[0].provider, 'github') def test_associate_with_partial_pipeline(self): self.do_login_with_partial_pipeline() - expect(len(self.user.social)).to.equal(1) - expect(self.user.social[0].provider).to.equal('github') + self.assertEqual(len(self.user.social), 1) + self.assertEqual(self.user.social[0].provider, 'github') class MultipleAccountsTest(AssociateActionTest): @@ -63,9 +62,9 @@ class MultipleAccountsTest(AssociateActionTest): def test_multiple_social_accounts(self): self.do_login() self.do_login(user_data_body=self.alternative_user_data_body) - expect(len(self.user.social)).to.equal(2) - expect(self.user.social[0].provider).to.equal('github') - expect(self.user.social[1].provider).to.equal('github') + self.assertEqual(len(self.user.social), 2) + self.assertEqual(self.user.social[0].provider, 'github') + self.assertEqual(self.user.social[1].provider, 'github') class AlreadyAssociatedErrorTest(BaseActionTest): @@ -83,7 +82,6 @@ def test_already_associated_error(self): self.user = self.user1 self.do_login() self.user = User(username='foobar2', email='foo2@bar2.com') - self.do_login.when.called_with().should.throw( - AuthAlreadyAssociated, - 'This github account is already in use.' - ) + with self.assertRaisesRegexp(AuthAlreadyAssociated, + 'This github account is already in use.'): + self.do_login() diff --git a/social/tests/actions/test_disconnect.py b/social/tests/actions/test_disconnect.py index 78c0f2ddb..328ad8d0c 100644 --- a/social/tests/actions/test_disconnect.py +++ b/social/tests/actions/test_disconnect.py @@ -1,6 +1,5 @@ import requests -from sure import expect from httpretty import HTTPretty from social.actions import do_disconnect @@ -15,16 +14,15 @@ class DisconnectActionTest(BaseActionTest): def test_not_allowed_to_disconnect(self): self.do_login() user = User.get(self.expected_username) - do_disconnect.when.called_with(self.backend, user).should.throw( - NotAllowedToDisconnect - ) + with self.assertRaises(NotAllowedToDisconnect): + do_disconnect(self.backend, user) def test_disconnect(self): self.do_login() user = User.get(self.expected_username) user.password = 'password' do_disconnect(self.backend, user) - expect(len(user.social)).to.equal(0) + self.assertEqual(len(user.social), 0) def test_disconnect_with_partial_pipeline(self): self.strategy.set_settings({ @@ -43,7 +41,7 @@ def test_disconnect_with_partial_pipeline(self): redirect = do_disconnect(self.backend, user) url = self.strategy.build_absolute_uri('/password') - expect(redirect.url).to.equal(url) + self.assertEqual(redirect.url, url) HTTPretty.register_uri(HTTPretty.GET, redirect.url, status=200, body='foobar') HTTPretty.register_uri(HTTPretty.POST, redirect.url, status=200) @@ -52,8 +50,8 @@ def test_disconnect_with_partial_pipeline(self): requests.get(url) requests.post(url, data={'password': password}) data = parse_qs(HTTPretty.last_request.body) - expect(data['password']).to.equal(password) + self.assertEqual(data['password'], password) self.strategy.session_set('password', data['password']) redirect = do_disconnect(self.backend, user) - expect(len(user.social)).to.equal(0) + self.assertEqual(len(user.social), 0) diff --git a/social/tests/actions/test_login.py b/social/tests/actions/test_login.py index e2339d4cc..905c38c43 100644 --- a/social/tests/actions/test_login.py +++ b/social/tests/actions/test_login.py @@ -1,5 +1,3 @@ -from sure import expect - from social.tests.models import User from social.tests.actions.actions import BaseActionTest @@ -17,13 +15,13 @@ def test_fields_stored_in_session(self): }) self.strategy.set_request_data({'foo': '1', 'bar': '2'}, self.backend) self.do_login() - expect(self.strategy.session_get('foo')).to.equal('1') - expect(self.strategy.session_get('bar')).to.equal('2') + self.assertEqual(self.strategy.session_get('foo'), '1') + self.assertEqual(self.strategy.session_get('bar'), '2') def test_redirect_value(self): self.strategy.set_request_data({'next': '/after-login'}, self.backend) redirect = self.do_login(after_complete_checks=False) - expect(redirect.url).to.equal('/after-login') + self.assertEqual(redirect.url, '/after-login') def test_login_with_invalid_partial_pipeline(self): def before_complete(): @@ -37,7 +35,7 @@ def test_new_user(self): 'SOCIAL_AUTH_NEW_USER_REDIRECT_URL': '/new-user' }) redirect = self.do_login(after_complete_checks=False) - expect(redirect.url).to.equal('/new-user') + self.assertEqual(redirect.url, '/new-user') def test_inactive_user(self): self.strategy.set_settings({ @@ -45,7 +43,7 @@ def test_inactive_user(self): }) User.set_active(False) redirect = self.do_login(after_complete_checks=False) - expect(redirect.url).to.equal('/inactive') + self.assertEqual(redirect.url, '/inactive') def test_invalid_user(self): self.strategy.set_settings({ @@ -64,4 +62,4 @@ def test_invalid_user(self): ) }) redirect = self.do_login(after_complete_checks=False) - expect(redirect.url).to.equal('/error') + self.assertEqual(redirect.url, '/error') diff --git a/social/tests/backends/base.py b/social/tests/backends/base.py index 15bb98fd3..94278655b 100644 --- a/social/tests/backends/base.py +++ b/social/tests/backends/base.py @@ -1,7 +1,6 @@ -import unittest +import unittest2 as unittest import requests -from sure import expect from httpretty import HTTPretty from social.utils import module_member, parse_qs @@ -62,22 +61,22 @@ def do_start(self): def do_login(self): user = self.do_start() username = self.expected_username - expect(user.username).to.equal(username) - expect(self.strategy.session_get('username')).to.equal(username) - expect(self.strategy.get_user(user.id)).to.equal(user) - expect(self.backend.get_user(user.id)).to.equal(user) + self.assertEqual(user.username, username) + self.assertEqual(self.strategy.session_get('username'), username) + self.assertEqual(self.strategy.get_user(user.id), user) + self.assertEqual(self.backend.get_user(user.id), user) user_backends = user_backends_data( user, self.strategy.get_setting('SOCIAL_AUTH_AUTHENTICATION_BACKENDS'), self.strategy.storage ) - expect(len(list(user_backends.keys()))).to.equal(3) - expect('associated' in user_backends).to.equal(True) - expect('not_associated' in user_backends).to.equal(True) - expect('backends' in user_backends).to.equal(True) - expect(len(user_backends['associated'])).to.equal(1) - expect(len(user_backends['not_associated'])).to.equal(1) - expect(len(user_backends['backends'])).to.equal(2) + self.assertEqual(len(list(user_backends.keys())), 3) + self.assertEqual('associated' in user_backends, True) + self.assertEqual('not_associated' in user_backends, True) + self.assertEqual('backends' in user_backends, True) + self.assertEqual(len(user_backends['associated']), 1) + self.assertEqual(len(user_backends['not_associated']), 1) + self.assertEqual(len(user_backends['backends']), 2) return user def pipeline_settings(self): @@ -111,7 +110,7 @@ def pipeline_password_handling(self, url): requests.post(url, data={'password': password}) data = parse_qs(HTTPretty.last_request.body) - expect(data['password']).to.equal(password) + self.assertEqual(data['password'], password) self.strategy.session_set('password', data['password']) return password @@ -121,7 +120,7 @@ def pipeline_slug_handling(self, url): requests.post(url, data={'slug': slug}) data = parse_qs(HTTPretty.last_request.body) - expect(data['slug']).to.equal(slug) + self.assertEqual(data['slug'], slug) self.strategy.session_set('slug', data['slug']) return slug @@ -129,28 +128,28 @@ def do_partial_pipeline(self): url = self.strategy.build_absolute_uri('/password') self.pipeline_settings() redirect = self.do_start() - expect(redirect.url).to.equal(url) + self.assertEqual(redirect.url, url) self.pipeline_handlers(url) password = self.pipeline_password_handling(url) data = self.strategy.session_pop('partial_pipeline') idx, backend, xargs, xkwargs = self.strategy.partial_from_session(data) - expect(backend).to.equal(self.backend.name) + self.assertEqual(backend, self.backend.name) redirect = self.backend.continue_pipeline(pipeline_index=idx, *xargs, **xkwargs) url = self.strategy.build_absolute_uri('/slug') - expect(redirect.url).to.equal(url) + self.assertEqual(redirect.url, url) self.pipeline_handlers(url) slug = self.pipeline_slug_handling(url) data = self.strategy.session_pop('partial_pipeline') idx, backend, xargs, xkwargs = self.strategy.partial_from_session(data) - expect(backend).to.equal(self.backend.name) + self.assertEqual(backend, self.backend.name) user = self.backend.continue_pipeline(pipeline_index=idx, *xargs, **xkwargs) - expect(user.username).to.equal(self.expected_username) - expect(user.slug).to.equal(slug) - expect(user.password).to.equal(password) + self.assertEqual(user.username, self.expected_username) + self.assertEqual(user.slug, slug) + self.assertEqual(user.password, password) return user diff --git a/social/tests/backends/legacy.py b/social/tests/backends/legacy.py index 4204fab11..a39079d9b 100644 --- a/social/tests/backends/legacy.py +++ b/social/tests/backends/legacy.py @@ -1,6 +1,5 @@ import requests -from sure import expect from httpretty import HTTPretty from social.utils import parse_qs @@ -40,7 +39,7 @@ def do_start(self): content_type='application/x-www-form-urlencoded' ) response = requests.get(start_url) - expect(response.text).to.equal(self.form.format(self.complete_url)) + self.assertEqual(response.text, self.form.format(self.complete_url)) response = requests.post( self.complete_url, data=parse_qs(self.response_body) diff --git a/social/tests/backends/oauth.py b/social/tests/backends/oauth.py index 50a1b425d..0169bd238 100644 --- a/social/tests/backends/oauth.py +++ b/social/tests/backends/oauth.py @@ -1,6 +1,5 @@ import requests -from sure import expect from httpretty import HTTPretty from social.p3 import urlparse @@ -75,8 +74,8 @@ def do_start(self): start_url = self.backend.start().url target_url = self.auth_handlers(start_url) response = requests.get(start_url) - expect(response.url).to.equal(target_url) - expect(response.text).to.equal('foobar') + self.assertEqual(response.url, target_url) + self.assertEqual(response.text, 'foobar') self.strategy.set_request_data(parse_qs(urlparse(target_url).query), self.backend) return self.backend.complete() diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index d7517330b..c15a91d39 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -200,9 +200,8 @@ def authtoken_raised(self, expected_message, **access_token_kwargs): self.access_token_body = self.prepare_access_token_body( **access_token_kwargs ) - self.do_login.when.called_with().should.throw( - AuthTokenError, expected_message - ) + with self.assertRaisesRegexp(AuthTokenError, expected_message): + self.do_login() def test_invalid_secret(self): self.authtoken_raised( diff --git a/social/tests/backends/test_bitbucket.py b/social/tests/backends/test_bitbucket.py index c59b9e577..3e8f244f5 100644 --- a/social/tests/backends/test_bitbucket.py +++ b/social/tests/backends/test_bitbucket.py @@ -1,4 +1,5 @@ import json + from httpretty import HTTPretty from social.p3 import urlencode @@ -64,12 +65,12 @@ def test_login(self): self.strategy.set_settings({ 'SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY': True }) - super(BitbucketOAuth1FailTest, self).test_login \ - .when.called_with().should.throw(AuthForbidden) + with self.assertRaises(AuthForbidden): + super(BitbucketOAuth1FailTest, self).test_login() def test_partial_pipeline(self): self.strategy.set_settings({ 'SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY': True }) - super(BitbucketOAuth1FailTest, self).test_partial_pipeline \ - .when.called_with().should.throw(AuthForbidden) + with self.assertRaises(AuthForbidden): + super(BitbucketOAuth1FailTest, self).test_partial_pipeline() diff --git a/social/tests/backends/test_box.py b/social/tests/backends/test_box.py index 7ab3844d7..d458ed5a2 100644 --- a/social/tests/backends/test_box.py +++ b/social/tests/backends/test_box.py @@ -1,7 +1,5 @@ import json -from sure import expect - from social.tests.backends.oauth import OAuth2Test @@ -66,6 +64,5 @@ def refresh_token_arguments(self): def test_refresh_token(self): user, social = self.do_refresh_token() - expect(social.extra_data['access_token']).to.equal( - 'T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl' - ) + self.assertEqual(social.extra_data['access_token'], + 'T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl') diff --git a/social/tests/backends/test_broken.py b/social/tests/backends/test_broken.py index b341cc515..4ad082901 100644 --- a/social/tests/backends/test_broken.py +++ b/social/tests/backends/test_broken.py @@ -1,4 +1,4 @@ -import unittest +import unittest2 as unittest from social.backends.base import BaseAuth @@ -15,25 +15,21 @@ def tearDown(self): self.backend = None def test_auth_url(self): - self.backend.auth_url.when.called_with().should.throw( - NotImplementedError, - 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, + 'Implement in subclass'): + self.backend.auth_url() def test_auth_html(self): - self.backend.auth_html.when.called_with().should.throw( - NotImplementedError, - 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, + 'Implement in subclass'): + self.backend.auth_html() def test_auth_complete(self): - self.backend.auth_complete.when.called_with().should.throw( - NotImplementedError, - 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, + 'Implement in subclass'): + self.backend.auth_complete() def test_get_user_details(self): - self.backend.get_user_details.when.called_with(None).should.throw( - NotImplementedError, - 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, + 'Implement in subclass'): + self.backend.get_user_details(None) diff --git a/social/tests/backends/test_dummy.py b/social/tests/backends/test_dummy.py index b7bb1e445..4ceab7558 100644 --- a/social/tests/backends/test_dummy.py +++ b/social/tests/backends/test_dummy.py @@ -2,7 +2,6 @@ import datetime import time -from sure import expect from httpretty import HTTPretty from social.actions import do_disconnect @@ -65,7 +64,7 @@ def test_partial_pipeline(self): def test_tokens(self): user = self.do_login() - expect(user.social[0].tokens).to.equal('foobar') + self.assertEqual(user.social[0].tokens, 'foobar') def test_revoke_token(self): self.strategy.set_settings({ @@ -91,7 +90,8 @@ def test_invalid_login(self): self.strategy.set_settings({ 'SOCIAL_AUTH_WHITELISTED_EMAILS': ['foo2@bar.com'] }) - self.do_login.when.called_with().should.throw(AuthForbidden) + with self.assertRaises(AuthForbidden): + self.do_login() class WhitelistDomainsTest(DummyOAuth2Test): @@ -105,7 +105,8 @@ def test_invalid_login(self): self.strategy.set_settings({ 'SOCIAL_AUTH_WHITELISTED_EMAILS': ['bar2.com'] }) - self.do_login.when.called_with().should.throw(AuthForbidden) + with self.assertRaises(AuthForbidden): + self.do_login() DELTA = datetime.timedelta(days=1) @@ -127,4 +128,4 @@ def test_expires_time(self): user = self.do_login() social = user.social[0] expiration = social.expiration_datetime() - expect(expiration <= DELTA).to.equal(True) + self.assertEqual(expiration <= DELTA, True) diff --git a/social/tests/backends/test_evernote.py b/social/tests/backends/test_evernote.py index cae7421a7..f83907333 100644 --- a/social/tests/backends/test_evernote.py +++ b/social/tests/backends/test_evernote.py @@ -34,17 +34,21 @@ class EvernoteOAuth1CanceledTest(EvernoteOAuth1Test): access_token_status = 401 def test_login(self): - self.do_login.when.called_with().should.throw(AuthCanceled) + with self.assertRaises(AuthCanceled): + self.do_login() def test_partial_pipeline(self): - self.do_partial_pipeline.when.called_with().should.throw(AuthCanceled) + with self.assertRaises(AuthCanceled): + self.do_partial_pipeline() class EvernoteOAuth1ErrorTest(EvernoteOAuth1Test): access_token_status = 500 def test_login(self): - self.do_login.when.called_with().should.throw(HTTPError) + with self.assertRaises(HTTPError): + self.do_login() def test_partial_pipeline(self): - self.do_partial_pipeline.when.called_with().should.throw(HTTPError) + with self.assertRaises(HTTPError): + self.do_partial_pipeline() diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py index bcaf0f0f5..6fe7aa66d 100644 --- a/social/tests/backends/test_facebook.py +++ b/social/tests/backends/test_facebook.py @@ -37,9 +37,9 @@ class FacebookOAuth2WrongUserDataTest(FacebookOAuth2Test): user_data_body = 'null' def test_login(self): - self.do_login.when.called_with().should.throw(AuthUnknownError) + with self.assertRaises(AuthUnknownError): + self.do_login() def test_partial_pipeline(self): - self.do_partial_pipeline.when.called_with().should.throw( - AuthUnknownError - ) + with self.assertRaises(AuthUnknownError): + self.do_partial_pipeline() diff --git a/social/tests/backends/test_github.py b/social/tests/backends/test_github.py index e09001013..5ce2c26d8 100644 --- a/social/tests/backends/test_github.py +++ b/social/tests/backends/test_github.py @@ -140,12 +140,13 @@ def auth_handlers(self, start_url): def test_login(self): self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ORG_NAME': 'foobar'}) - self.do_login.when.called_with().should.throw(AuthFailed) + with self.assertRaises(AuthFailed): + self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ORG_NAME': 'foobar'}) - self.do_partial_pipeline.when.called_with().should.throw(AuthFailed) - + with self.assertRaises(AuthFailed): + self.do_partial_pipeline() class GithubTeamOAuth2Test(GithubOAuth2Test): @@ -181,8 +182,10 @@ def auth_handlers(self, start_url): def test_login(self): self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_TEAM_ID': '123'}) - self.do_login.when.called_with().should.throw(AuthFailed) + with self.assertRaises(AuthFailed): + self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_TEAM_ID': '123'}) - self.do_partial_pipeline.when.called_with().should.throw(AuthFailed) + with self.assertRaises(AuthFailed): + self.do_partial_pipeline() diff --git a/social/tests/backends/test_livejournal.py b/social/tests/backends/test_livejournal.py index 041ce3f04..0d1142025 100644 --- a/social/tests/backends/test_livejournal.py +++ b/social/tests/backends/test_livejournal.py @@ -1,4 +1,3 @@ -# import json import datetime from httpretty import HTTPretty @@ -17,13 +16,13 @@ class LiveJournalOpenIdTest(OpenIdTest): expected_username = 'foobar' discovery_body = ''.join([ '', - '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://www.livejournal.com/openid/server.bml', - 'http://foobar.livejournal.com/', - '', - '', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://www.livejournal.com/openid/server.bml', + 'http://foobar.livejournal.com/', + '', + '', '' ]) server_response = urlencode({ @@ -96,6 +95,5 @@ def test_partial_pipeline(self): def test_failed_login(self): self._setup_handlers() - self.do_login.when.called_with().should.throw( - AuthMissingParameter - ) + with self.assertRaises(AuthMissingParameter): + self.do_login() diff --git a/social/tests/backends/test_reddit.py b/social/tests/backends/test_reddit.py index e0b6b5ff3..6caf54c74 100644 --- a/social/tests/backends/test_reddit.py +++ b/social/tests/backends/test_reddit.py @@ -1,7 +1,5 @@ import json -from sure import expect - from social.tests.backends.oauth import OAuth2Test @@ -58,4 +56,4 @@ def refresh_token_arguments(self): def test_refresh_token(self): user, social = self.do_refresh_token() - expect(social.extra_data['access_token']).to.equal('foobar-new-token') + self.assertEqual(social.extra_data['access_token'], 'foobar-new-token') diff --git a/social/tests/backends/test_steam.py b/social/tests/backends/test_steam.py index 7e8957fbb..09cf52460 100644 --- a/social/tests/backends/test_steam.py +++ b/social/tests/backends/test_steam.py @@ -19,23 +19,23 @@ class SteamOpenIdTest(OpenIdTest): discovery_body = ''.join([ '', '', - '', - '', - 'http://specs.openid.net/auth/2.0/server', - 'https://steamcommunity.com/openid/login', - '', - '', + '', + '', + 'http://specs.openid.net/auth/2.0/server', + 'https://steamcommunity.com/openid/login', + '', + '', '' ]) user_discovery_body = ''.join([ '', - '', - '', - '', - 'http://specs.openid.net/auth/2.0/signon ', - 'https://steamcommunity.com/openid/login', - '', - '', + '', + '', + '', + 'http://specs.openid.net/auth/2.0/signon ', + 'https://steamcommunity.com/openid/login', + '', + '', '' ]) server_response = urlencode({ @@ -47,8 +47,8 @@ class SteamOpenIdTest(OpenIdTest): 'openid.identity': 'https://steamcommunity.com/openid/id/123', 'openid.return_to': 'http://myapp.com/complete/steam/?' 'janrain_nonce=' + JANRAIN_NONCE, - 'openid.response_nonce': JANRAIN_NONCE + - 'oD4UZ3w9chOAiQXk0AqDipqFYRA=', + 'openid.response_nonce': + JANRAIN_NONCE + 'oD4UZ3w9chOAiQXk0AqDipqFYRA=', 'openid.assoc_handle': '1234567890', 'openid.signed': 'signed,op_endpoint,claimed_id,identity,return_to,' 'response_nonce,assoc_handle', @@ -116,8 +116,8 @@ class SteamOpenIdMissingSteamIdTest(SteamOpenIdTest): 'openid.identity': 'https://steamcommunity.com/openid/BROKEN', 'openid.return_to': 'http://myapp.com/complete/steam/?' 'janrain_nonce=' + JANRAIN_NONCE, - 'openid.response_nonce': JANRAIN_NONCE + - 'oD4UZ3w9chOAiQXk0AqDipqFYRA=', + 'openid.response_nonce': + JANRAIN_NONCE + 'oD4UZ3w9chOAiQXk0AqDipqFYRA=', 'openid.assoc_handle': '1234567890', 'openid.signed': 'signed,op_endpoint,claimed_id,identity,return_to,' 'response_nonce,assoc_handle', @@ -126,8 +126,10 @@ class SteamOpenIdMissingSteamIdTest(SteamOpenIdTest): def test_login(self): self._login_setup(user_url='https://steamcommunity.com/openid/BROKEN') - self.do_login.when.called_with().should.throw(AuthFailed) + with self.assertRaises(AuthFailed): + self.do_login() def test_partial_pipeline(self): self._login_setup(user_url='https://steamcommunity.com/openid/BROKEN') - self.do_partial_pipeline.when.called_with().should.throw(AuthFailed) + with self.assertRaises(AuthFailed): + self.do_partial_pipeline() diff --git a/social/tests/backends/test_utils.py b/social/tests/backends/test_utils.py index 9ba25d2b1..244f899a3 100644 --- a/social/tests/backends/test_utils.py +++ b/social/tests/backends/test_utils.py @@ -1,5 +1,4 @@ -import unittest -from sure import expect +import unittest2 as unittest from social.tests.models import TestStorage from social.tests.strategy import TestStrategy @@ -25,11 +24,11 @@ def test_load_backends(self): ), force_load=True) keys = list(loaded_backends.keys()) keys.sort() - expect(keys).to.equal(['facebook', 'flickr', 'github']) + self.assertEqual(keys, ['facebook', 'flickr', 'github']) backends = () loaded_backends = load_backends(backends, force_load=True) - expect(len(list(loaded_backends.keys()))).to.equal(0) + self.assertEqual(len(list(loaded_backends.keys())), 0) class GetBackendTest(BaseBackendUtilsTest): @@ -39,13 +38,12 @@ def test_get_backend(self): 'social.backends.facebook.FacebookOAuth2', 'social.backends.flickr.FlickrOAuth' ), 'github') - expect(backend).to.equal(GithubOAuth2) + self.assertEqual(backend, GithubOAuth2) def test_get_missing_backend(self): - get_backend.when.called_with(( - 'social.backends.github.GithubOAuth2', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.flickr.FlickrOAuth' - ), 'foobar').should.throw( - MissingBackend, 'Missing backend "foobar" entry' - ) + with self.assertRaisesRegexp(MissingBackend, + 'Missing backend "foobar" entry'): + get_backend(('social.backends.github.GithubOAuth2', + 'social.backends.facebook.FacebookOAuth2', + 'social.backends.flickr.FlickrOAuth'), + 'foobar') diff --git a/social/tests/requirements-python3.txt b/social/tests/requirements-python3.txt new file mode 100644 index 000000000..5cc5bfc2d --- /dev/null +++ b/social/tests/requirements-python3.txt @@ -0,0 +1,7 @@ +httpretty==0.6.5 +coverage>=3.6 +mock==1.0.1 +nose>=1.2.1 +requests>=1.1.0 +PyJWT==0.2.1 +unittest2py3k==0.5.1 diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index ec3f110dd..e82544764 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,5 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -sure==1.2.3 PyJWT==0.2.1 +unittest2==0.5.1 diff --git a/social/tests/test_exceptions.py b/social/tests/test_exceptions.py index be111a1e9..2aab43655 100644 --- a/social/tests/test_exceptions.py +++ b/social/tests/test_exceptions.py @@ -1,5 +1,4 @@ -import unittest -from sure import expect +import unittest2 as unittest from social.exceptions import SocialAuthBaseException, WrongBackend, \ AuthFailed, AuthTokenError, \ @@ -20,7 +19,7 @@ def test_exception_message(self): try: raise self.exception except SocialAuthBaseException as err: - expect(str(err)).to.equal(self.expected_message) + self.assertEqual(str(err), self.expected_message) class WrongBackendTest(BaseExceptionTestCase): diff --git a/social/tests/test_pipeline.py b/social/tests/test_pipeline.py index c2c2c4710..d1911c509 100644 --- a/social/tests/test_pipeline.py +++ b/social/tests/test_pipeline.py @@ -1,7 +1,5 @@ import json -from sure import expect - from social.exceptions import AuthException from social.tests.models import TestUserSocialAuth, TestStorage, User @@ -70,7 +68,8 @@ def setUp(self): super(UnknownErrorOnLoginTest, self).setUp() def test_unknown_error(self): - self.do_login.when.called_with().should.throw(UnknownError) + with self.assertRaises(UnknownError): + self.do_login() class EmailAsUsernameTest(BaseActionTest): @@ -167,8 +166,8 @@ class RepeatedUsernameTest(BaseActionTest): def test_random_username(self): User(username='foobar') self.do_login(after_complete_checks=False) - expect(self.strategy.session_get('username').startswith('foobar')) \ - .to.equal(True) + self.assertTrue(self.strategy.session_get('username') + .startswith('foobar')) class AssociateByEmailTest(BaseActionTest): @@ -176,8 +175,8 @@ def test_multiple_accounts_with_same_email(self): user = User(username='foobar1') user.email = 'foo@bar.com' self.do_login(after_complete_checks=False) - expect(self.strategy.session_get('username').startswith('foobar')) \ - .to.equal(True) + self.assertTrue(self.strategy.session_get('username') + .startswith('foobar')) class MultipleAccountsWithSameEmailTest(BaseActionTest): @@ -186,8 +185,9 @@ def test_multiple_accounts_with_same_email(self): user2 = User(username='foobar2') user1.email = 'foo@bar.com' user2.email = 'foo@bar.com' - self.do_login.when.called_with(after_complete_checks=False)\ - .should.throw(AuthException) + with self.assertRaises(AuthException): + self.do_login(after_complete_checks=False) + class UserPersistsInPartialPipeline(BaseActionTest): def test_user_persists_in_partial_pipeline_kwargs(self): @@ -203,7 +203,7 @@ def test_user_persists_in_partial_pipeline_kwargs(self): ) }) - redirect = self.do_login(after_complete_checks=False) + self.do_login(after_complete_checks=False) # Handle the partial pipeline self.strategy.session_set('attribute', 'testing') @@ -215,7 +215,6 @@ def test_user_persists_in_partial_pipeline_kwargs(self): self.backend.continue_pipeline(pipeline_index=idx, *xargs, **xkwargs) - def test_user_persists_in_partial_pipeline(self): user = User(username='foobar1') user.email = 'foo@bar.com' @@ -229,7 +228,7 @@ def test_user_persists_in_partial_pipeline(self): ) }) - redirect = self.do_login(after_complete_checks=False) + self.do_login(after_complete_checks=False) # Handle the partial pipeline self.strategy.session_set('attribute', 'testing') diff --git a/social/tests/test_storage.py b/social/tests/test_storage.py index 8e442fe9d..a1d1a4c50 100644 --- a/social/tests/test_storage.py +++ b/social/tests/test_storage.py @@ -1,8 +1,6 @@ import six import random -import unittest - -from sure import expect +import unittest2 as unittest from social.strategies.base import BaseStrategy from social.storage.base import UserMixin, NonceMixin, AssociationMixin, \ @@ -11,6 +9,9 @@ from social.tests.models import User +NOT_IMPLEMENTED_MSG = 'Implement in subclass' + + class BrokenUser(UserMixin): pass @@ -41,6 +42,7 @@ class BrokenStorage(BaseStorage): class BrokenUserTests(unittest.TestCase): + def setUp(self): self.user = BrokenUser @@ -48,43 +50,36 @@ def tearDown(self): self.user = None def test_get_username(self): - self.user.get_username.when.called_with(User('foobar')).should.throw( - NotImplementedError, 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.get_username(User('foobar')) def test_user_model(self): - self.user.user_model.when.called_with().should.throw( - NotImplementedError, 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.user_model() def test_username_max_length(self): - self.user.username_max_length.when.called_with().should.throw( - NotImplementedError, 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.username_max_length() def test_get_user(self): - self.user.get_user.when.called_with(1).should.throw( - NotImplementedError, 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.get_user(1) def test_get_social_auth(self): - self.user.get_social_auth.when.called_with('foo', 1).should.throw( - NotImplementedError, 'Implement in subclass' - ) + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.get_social_auth('foo', 1) def test_get_social_auth_for_user(self): - self.user.get_social_auth_for_user.when.called_with(User('foobar')) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.get_social_auth_for_user(User('foobar')) def test_create_social_auth(self): - self.user.create_social_auth.when \ - .called_with(User('foobar'), 1, 'foo') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.create_social_auth(User('foobar'), 1, 'foo') def test_disconnect(self): - self.user.disconnect\ - .when.called_with(BrokenUser())\ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.user.disconnect(BrokenUser()) class BrokenAssociationTests(unittest.TestCase): @@ -95,17 +90,16 @@ def tearDown(self): self.association = None def test_store(self): - self.association.store.when \ - .called_with('http://foobar.com', BrokenAssociation()) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.association.store('http://foobar.com', BrokenAssociation()) def test_get(self): - self.association.get.when.called_with() \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.association.get() def test_remove(self): - self.association.remove.when.called_with([1, 2, 3]) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.association.remove([1, 2, 3]) class BrokenNonceTests(unittest.TestCase): @@ -116,9 +110,8 @@ def tearDown(self): self.nonce = None def test_use(self): - self.nonce.use.when \ - .called_with('http://foobar.com', 1364951922, 'foobar123') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.nonce.use('http://foobar.com', 1364951922, 'foobar123') class BrokenCodeTest(unittest.TestCase): @@ -129,9 +122,8 @@ def tearDown(self): self.code = None def test_get_code(self): - self.code.get_code.when \ - .called_with('foobar') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.code.get_code('foobar') class BrokenStrategyTests(unittest.TestCase): @@ -142,73 +134,61 @@ def tearDown(self): self.strategy = None def test_redirect(self): - self.strategy.redirect.when \ - .called_with('http://foobar.com') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.redirect('http://foobar.com') def test_get_setting(self): - self.strategy.get_setting.when \ - .called_with('foobar') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.get_setting('foobar') def test_html(self): - self.strategy.html.when \ - .called_with('

          foobar

          ') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.html('

          foobar

          ') def test_request_data(self): - self.strategy.request_data.when \ - .called_with() \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.request_data() def test_request_host(self): - self.strategy.request_host.when \ - .called_with() \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.request_host() def test_session_get(self): - self.strategy.session_get.when \ - .called_with('foobar') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.session_get('foobar') def test_session_set(self): - self.strategy.session_set.when \ - .called_with('foobar', 123) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.session_set('foobar', 123) def test_session_pop(self): - self.strategy.session_pop.when \ - .called_with('foobar') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.session_pop('foobar') def test_build_absolute_uri(self): - self.strategy.build_absolute_uri.when \ - .called_with('/foobar') \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.build_absolute_uri('/foobar') def test_render_html_with_tpl(self): - self.strategy.render_html.when \ - .called_with('foobar.html', context={}) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.render_html('foobar.html', context={}) def test_render_html_with_html(self): - self.strategy.render_html.when \ - .called_with(html='

          foobar

          ', context={}) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.render_html(html='

          foobar

          ', context={}) def test_render_html_with_none(self): - self.strategy.render_html.when \ - .called_with() \ - .should.throw(ValueError, 'Missing template or html parameters') + with self.assertRaisesRegexp(ValueError, + 'Missing template or html parameters'): + self.strategy.render_html() def test_is_integrity_error(self): - self.strategy.storage.is_integrity_error.when \ - .called_with(None) \ - .should.throw(NotImplementedError, 'Implement in subclass') + with self.assertRaisesRegexp(NotImplementedError, NOT_IMPLEMENTED_MSG): + self.strategy.storage.is_integrity_error(None) def test_random_string(self): - expect(isinstance(self.strategy.random_string(), six.string_types)) \ - .to.equal(True) + self.assertTrue(isinstance(self.strategy.random_string(), + six.string_types)) def test_random_string_without_systemrandom(self): def SystemRandom(): @@ -218,6 +198,5 @@ def SystemRandom(): random.SystemRandom = SystemRandom strategy = BrokenStrategyWithSettings(storage=BrokenStorage) - expect(isinstance(strategy.random_string(), six.string_types)) \ - .to.equal(True) + self.assertTrue(isinstance(strategy.random_string(), six.string_types)) random.SystemRandom = orig_random diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index bde7a8463..7bd8db089 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -1,8 +1,7 @@ import sys -import unittest +import unittest2 as unittest from mock import Mock -from sure import expect from social.utils import sanitize_redirect, user_is_authenticated, \ user_is_active, slugify, build_absolute_uri, \ @@ -14,82 +13,81 @@ class SanitizeRedirectTest(unittest.TestCase): def test_none_redirect(self): - expect(sanitize_redirect('myapp.com', None)).to.equal(None) + self.assertEqual(sanitize_redirect('myapp.com', None), None) def test_empty_redirect(self): - expect(sanitize_redirect('myapp.com', '')).to.equal(None) + self.assertEqual(sanitize_redirect('myapp.com', ''), None) def test_dict_redirect(self): - expect(sanitize_redirect('myapp.com', {})).to.equal(None) + self.assertEqual(sanitize_redirect('myapp.com', {}), None) def test_invalid_redirect(self): - expect(sanitize_redirect('myapp.com', - {'foo': 'bar'})).to.equal(None) + self.assertEqual(sanitize_redirect('myapp.com', {'foo': 'bar'}), None) def test_wrong_path_redirect(self): - expect(sanitize_redirect( - 'myapp.com', - 'http://notmyapp.com/path/' - )).to.equal(None) + self.assertEqual( + sanitize_redirect('myapp.com', 'http://notmyapp.com/path/'), + None + ) def test_valid_absolute_redirect(self): - expect(sanitize_redirect( - 'myapp.com', + self.assertEqual( + sanitize_redirect('myapp.com', 'http://myapp.com/path/'), 'http://myapp.com/path/' - )).to.equal('http://myapp.com/path/') + ) def test_valid_relative_redirect(self): - expect(sanitize_redirect('myapp.com', '/path/')).to.equal('/path/') + self.assertEqual(sanitize_redirect('myapp.com', '/path/'), '/path/') class UserIsAuthenticatedTest(unittest.TestCase): def test_user_is_none(self): - expect(user_is_authenticated(None)).to.equal(False) + self.assertEqual(user_is_authenticated(None), False) def test_user_is_not_none(self): - expect(user_is_authenticated(object())).to.equal(True) + self.assertEqual(user_is_authenticated(object()), True) def test_user_has_is_authenticated(self): class User(object): is_authenticated = True - expect(user_is_authenticated(User())).to.equal(True) + self.assertEqual(user_is_authenticated(User()), True) def test_user_has_is_authenticated_callable(self): class User(object): def is_authenticated(self): return True - expect(user_is_authenticated(User())).to.equal(True) + self.assertEqual(user_is_authenticated(User()), True) class UserIsActiveTest(unittest.TestCase): def test_user_is_none(self): - expect(user_is_active(None)).to.equal(False) + self.assertEqual(user_is_active(None), False) def test_user_is_not_none(self): - expect(user_is_active(object())).to.equal(True) + self.assertEqual(user_is_active(object()), True) def test_user_has_is_active(self): class User(object): is_active = True - expect(user_is_active(User())).to.equal(True) + self.assertEqual(user_is_active(User()), True) def test_user_has_is_active_callable(self): class User(object): def is_active(self): return True - expect(user_is_active(User())).to.equal(True) + self.assertEqual(user_is_active(User()), True) class SlugifyTest(unittest.TestCase): def test_slugify_formats(self): if PY3: - expect(slugify('FooBar')).to.equal('foobar') - expect(slugify('Foo Bar')).to.equal('foo-bar') - expect(slugify('Foo (Bar)')).to.equal('foo-bar') + self.assertEqual(slugify('FooBar'), 'foobar') + self.assertEqual(slugify('Foo Bar'), 'foo-bar') + self.assertEqual(slugify('Foo (Bar)'), 'foo-bar') else: - expect(slugify('FooBar'.decode('utf-8'))).to.equal('foobar') - expect(slugify('Foo Bar'.decode('utf-8'))).to.equal('foo-bar') - expect(slugify('Foo (Bar)'.decode('utf-8'))).to.equal('foo-bar') + self.assertEqual(slugify('FooBar'.decode('utf-8')), 'foobar') + self.assertEqual(slugify('Foo Bar'.decode('utf-8')), 'foo-bar') + self.assertEqual(slugify('Foo (Bar)'.decode('utf-8')), 'foo-bar') class BuildAbsoluteURITest(unittest.TestCase): @@ -100,41 +98,43 @@ def tearDown(self): self.host = None def test_path_none(self): - expect(build_absolute_uri(self.host)).to.equal(self.host) + self.assertEqual(build_absolute_uri(self.host), self.host) def test_path_empty(self): - expect(build_absolute_uri(self.host, '')).to.equal(self.host) + self.assertEqual(build_absolute_uri(self.host, ''), self.host) def test_path_http(self): - expect(build_absolute_uri(self.host, 'http://barfoo.com')) \ - .to.equal('http://barfoo.com') + self.assertEqual(build_absolute_uri(self.host, 'http://barfoo.com'), + 'http://barfoo.com') def test_path_https(self): - expect(build_absolute_uri(self.host, 'https://barfoo.com')) \ - .to.equal('https://barfoo.com') + self.assertEqual(build_absolute_uri(self.host, 'https://barfoo.com'), + 'https://barfoo.com') def test_host_ends_with_slash_and_path_starts_with_slash(self): - expect(build_absolute_uri(self.host + '/', '/foo/bar')) \ - .to.equal('http://foobar.com/foo/bar') + self.assertEqual(build_absolute_uri(self.host + '/', '/foo/bar'), + 'http://foobar.com/foo/bar') def test_absolute_uri(self): - expect(build_absolute_uri(self.host, '/foo/bar')) \ - .to.equal('http://foobar.com/foo/bar') + self.assertEqual(build_absolute_uri(self.host, '/foo/bar'), + 'http://foobar.com/foo/bar') class PartialPipelineData(unittest.TestCase): def test_kwargs_included_in_result(self): backend = self._backend() - kwargitem = ('foo', 'bar') + key, val = ('foo', 'bar') _, xkwargs = partial_pipeline_data(backend, None, - *(), **dict([kwargitem])) - xkwargs.should.have.key(kwargitem[0]).being.equal(kwargitem[1]) + *(), **dict([(key, val)])) + self.assertTrue(key in xkwargs) + self.assertEqual(xkwargs[key], val) def test_update_user(self): user = object() backend = self._backend(session_kwargs={'user': None}) _, xkwargs = partial_pipeline_data(backend, user) - xkwargs.should.have.key('user').being.equal(user) + self.assertTrue('user' in xkwargs) + self.assertEqual(xkwargs['user'], user) def _backend(self, session_kwargs=None): strategy = Mock() diff --git a/tox.ini b/tox.ini index f5ea73d55..298244e4d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,18 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py33, py34, doc +envlist = py26, py27, py33, py34, pypy, doc [testenv] commands = nosetests --where=social/tests --stop deps = -r{toxinidir}/social/tests/requirements.txt +[testenv:py33] +deps = -r{toxinidir}/social/tests/requirements-python3.txt + +[testenv:py34] +deps = -r{toxinidir}/social/tests/requirements-python3.txt + [testenv:doc] changedir = docs deps = sphinx From 1b1f9a33990b1de1250bfd05d0a02cfde4e1a011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Oct 2014 13:13:08 -0200 Subject: [PATCH 377/890] Travisci update --- .travis.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index bfab16118..cfb9e6402 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,19 @@ language: python +env: + global: + - TEST_REQUIREMENTS=social/tests/requirements.txt python: - "2.6" - "2.7" - - "3.3" - - "3.4" - "pypy" +matrix: + include: + - python: "3.3" + env: TEST_REQUIREMENTS=social/tests/requirements-python3.txt + - python: "3.4" + env: TEST_REQUIREMENTS=social/tests/requirements-python3.txt install: - "python setup.py -q install" - - "travis_retry pip install -r social/tests/requirements.txt" + - "travis_retry pip install -r $TEST_REQUIREMENTS" script: - "nosetests --with-coverage --cover-package=social --where=social/tests" From a005ebde758b3e7cabc9f6e9fd355e3359cb96af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Oct 2014 13:58:33 -0200 Subject: [PATCH 378/890] PEP8 and module rename --- social/apps/django_app/default/__init__.py | 3 ++- social/apps/django_app/default/{apps.py => config.py} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename social/apps/django_app/default/{apps.py => config.py} (100%) diff --git a/social/apps/django_app/default/__init__.py b/social/apps/django_app/default/__init__.py index 6f0c46adc..99c8c8e4d 100644 --- a/social/apps/django_app/default/__init__.py +++ b/social/apps/django_app/default/__init__.py @@ -6,4 +6,5 @@ * In urls.py include url('', include('social.apps.django_app.urls')) """ -default_app_config = 'social.apps.django_app.default.apps.PythonSocialAuthConfig' +default_app_config = \ + 'social.apps.django_app.default.config.PythonSocialAuthConfig' diff --git a/social/apps/django_app/default/apps.py b/social/apps/django_app/default/config.py similarity index 100% rename from social/apps/django_app/default/apps.py rename to social/apps/django_app/default/config.py From 1b808cf1280f503b4fdff779d0d606b05639b4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Karzy=C5=84ski?= Date: Wed, 8 Oct 2014 22:17:34 +0200 Subject: [PATCH 379/890] Salesforce OAuth2 support --- social/backends/salesforce.py | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 social/backends/salesforce.py diff --git a/social/backends/salesforce.py b/social/backends/salesforce.py new file mode 100644 index 000000000..5ca77e21b --- /dev/null +++ b/social/backends/salesforce.py @@ -0,0 +1,49 @@ +from urllib import urlencode +from social.backends.oauth import BaseOAuth2 + + +class SalesforceOAuth2(BaseOAuth2): + """Salesforce OAuth2 authentication backend""" + name = 'salesforce-oauth2' + AUTHORIZATION_URL = 'https://login.salesforce.com/services/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token' + REVOKE_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/revoke' + ACCESS_TOKEN_METHOD = 'POST' + REFRESH_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ' ' + EXTRA_DATA = [ + ('id', 'id'), + ('instance_url', 'instance_url'), + ('issued_at', 'issued_at'), + ('signature', 'signature'), + ('refresh_token', 'refresh_token'), + ] + + def get_user_details(self, response): + """Return user details from a Salesforce account""" + return { + 'username': response.get('username'), + 'email': response.get('email') or '', + 'first_name': response.get('first_name'), + 'last_name': response.get('last_name'), + 'fullname': response.get('display_name') + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + user_id_url = kwargs.get('response').get('id') + url = user_id_url + '?' + urlencode({ + 'access_token': access_token + }) + try: + return self.get_json(url) + except ValueError: + return None + + +class SalesforceOAuth2Sandbox(SalesforceOAuth2): + """Salesforce OAuth2 authentication testing backend""" + name = 'salesforce-oauth2-sandbox' + AUTHORIZATION_URL = 'https://test.salesforce.com/services/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://test.salesforce.com/services/oauth2/token' + REVOKE_TOKEN_URL = 'https://test.salesforce.com/services/oauth2/revoke' From bee9518d75f65f335170fdd0d5533e2b1163452b Mon Sep 17 00:00:00 2001 From: SilentSokolov Date: Sun, 12 Oct 2014 10:23:03 +0400 Subject: [PATCH 380/890] Fix does not match the number of arguments (for vk and ok backend) --- docs/backends/vk.rst | 2 +- social/backends/odnoklassniki.py | 2 +- social/backends/vk.py | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/backends/vk.rst b/docs/backends/vk.rst index ea9a2f373..2b5deb57a 100644 --- a/docs/backends/vk.rst +++ b/docs/backends/vk.rst @@ -38,7 +38,7 @@ To support OAuth2 authentication for VK.com applications: - Fill ``Application ID`` and ``Application Secret`` settings:: - SOCIAL_AUTH_VK_APP_ID = '' + SOCIAL_AUTH_VK_APP_KEY = '' SOCIAL_AUTH_VK_APP_SECRET = '' - Fill ``user_mode``:: diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index f1422ee57..f639b79b1 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -67,7 +67,7 @@ def get_user_details(self, response): 'last_name': last_name } - def auth_complete(self, request, user, *args, **kwargs): + def auth_complete(self, *args, **kwargs): self.verify_auth_sig() response = self.get_response() fields = ('uid', 'first_name', 'last_name', 'name') + \ diff --git a/social/backends/vk.py b/social/backends/vk.py index f633f5f58..e72926853 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -85,6 +85,9 @@ class VKOAuth2(BaseOAuth2): ('expires_in', 'expires') ] + def get_user_id(self, details, response): + return response['uid'] + def get_user_details(self, response): """Return user details from VK.com account""" fullname, first_name, last_name = self.get_user_names( @@ -97,7 +100,7 @@ def get_user_details(self, response): 'first_name': first_name, 'last_name': last_name} - def user_data(self, access_token, response, *args, **kwargs): + def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" request_data = ['first_name', 'last_name', 'screen_name', 'nickname', 'photo'] + self.setting('EXTRA_DATA', []) @@ -106,7 +109,6 @@ def user_data(self, access_token, response, *args, **kwargs): data = vk_api(self, 'users.get', { 'access_token': access_token, 'fields': fields, - 'uids': response.get('user_id') }) if data.get('error'): From ca24550e49de098bc5c80791542560c5bef1da75 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Thu, 16 Oct 2014 23:19:55 +0200 Subject: [PATCH 381/890] Fix migration issue on python 3 Django calls ``related_name % {...}`` in RelatedField.contribute_to_class which does not work on Python 3 as it does not support string formatting on byte-strings. --- social/apps/django_app/default/migrations/0001_initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 42c9bb45b..eaa7e4051 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -75,7 +75,7 @@ class Migration(migrations.Migration): ('extra_data', social.apps.django_app.default.fields.JSONField( default=b'{}')), ('user', models.ForeignKey( - related_name=b'social_auth', to=settings.AUTH_USER_MODEL)), + related_name='social_auth', to=settings.AUTH_USER_MODEL)), ], options={ 'db_table': 'social_auth_usersocialauth', From 6550eaee8ff94f4b2cb7dec7f704022796704612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 23 Oct 2014 01:23:48 -0200 Subject: [PATCH 382/890] Pick github primary email first. Fixes #413 --- social/backends/github.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/social/backends/github.py b/social/backends/github.py index 30f98122e..db58cc1fa 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -36,13 +36,21 @@ def user_data(self, access_token, *args, **kwargs): data = self._user_data(access_token) if not data.get('email'): try: - email = self._user_data(access_token, '/emails')[0] - except (HTTPError, IndexError, ValueError, TypeError): - email = '' - - if isinstance(email, dict): - email = email.get('email', '') - data['email'] = email + emails = self._user_data(access_token, '/emails') + except (HTTPError, ValueError, TypeError): + emails = [] + + if emails: + email = emails[0] + primary_emails = [e for e in emails + if not isinstance(e, dict) or + e.get('primary')] + + if primary_emails: + email = primary_emails[0] + if isinstance(email, dict): + email = email.get('email', '') + data['email'] = email return data def _user_data(self, access_token, path=None): @@ -85,7 +93,6 @@ def member_url(self, user_data): username=user_data.get('login')) - class GithubTeamOAuth2(GithubMemberOAuth2): """Github OAuth2 authentication backend for teams""" name = 'github-team' From a9c44398d2294bb8f947d17870f42515679df2fc Mon Sep 17 00:00:00 2001 From: John Lynn Date: Sun, 26 Oct 2014 11:10:26 -0400 Subject: [PATCH 383/890] Fix custom user model migrations for Django 1.7 UserSocialAuth migration was depending on user.Auth by default and not using the user model specified by SOCIAL_AUTH_USER_MODEL --- social/apps/django_app/default/migrations/0001_initial.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index eaa7e4051..fdbad18ac 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -5,12 +5,16 @@ import social.apps.django_app.default.fields from django.conf import settings import social.storage.django_orm +from social.utils import setting_name +user_model = getattr(settings, setting_name('USER_MODEL'), None) or \ + getattr(settings, 'AUTH_USER_MODE', None) or \ + 'auth.User' class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(user_model), ] operations = [ @@ -75,7 +79,7 @@ class Migration(migrations.Migration): ('extra_data', social.apps.django_app.default.fields.JSONField( default=b'{}')), ('user', models.ForeignKey( - related_name='social_auth', to=settings.AUTH_USER_MODEL)), + related_name='social_auth', to=user_model)), ], options={ 'db_table': 'social_auth_usersocialauth', From 4ec0982c581cf0ff0f307f0d8dbd56ce41567091 Mon Sep 17 00:00:00 2001 From: Mitchel Humpherys Date: Wed, 29 Oct 2014 15:48:02 -0700 Subject: [PATCH 384/890] use correct tense for `to meet' past participle --- docs/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installing.rst b/docs/installing.rst index f0a132a3d..c1f0cb426 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -4,7 +4,7 @@ Installation Dependencies ------------ -Dependencies that **must** be meet to use the application: +Dependencies that **must** be met to use the application: - OpenId_ support depends on python-openid_ From 9d99703e535d4f04381485d750a4b9785cb41cd8 Mon Sep 17 00:00:00 2001 From: Alex Parij Date: Wed, 29 Oct 2014 20:13:41 -0400 Subject: [PATCH 385/890] Update base.py Deleting a leftover constant that was moved to social.pipeline.utils --- social/strategies/base.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/social/strategies/base.py b/social/strategies/base.py index b25e214eb..e7e4d4d2d 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -34,10 +34,6 @@ class BaseStrategy(object): ALLOWED_CHARS = 'abcdefghijklmnopqrstuvwxyz' \ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '0123456789' - # well-known serializable types - SERIALIZABLE_TYPES = (dict, list, tuple, set, bool, type(None)) + \ - six.integer_types + six.string_types + \ - (six.text_type, six.binary_type,) DEFAULT_TEMPLATE_STRATEGY = BaseTemplateStrategy def __init__(self, storage=None, tpl=None): From 0d75600d9a3bd09d62f1c9fe50f81be4c941bde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 30 Oct 2014 23:16:49 -0200 Subject: [PATCH 386/890] Fix use case snippet --- docs/use_cases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index f7c2ec8b7..9fa95c69b 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -132,7 +132,7 @@ implemented easily):: # request.backend and request.strategy will be loaded with the current # backend and strategy. token = request.GET.get('access_token') - user = backend.do_auth(request.GET.get('access_token')) + user = request.backend.do_auth(request.GET.get('access_token')) if user: login(request, user) return 'OK' From d2989f4db686252636422d2ba76cd01b4a5b05df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 30 Oct 2014 23:17:53 -0200 Subject: [PATCH 387/890] Link missing doc --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index b1dcfc1f7..74b5b5dae 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -50,6 +50,7 @@ Social backends angel aol appsfuel + battlenet beats behance belgium_eid From 47042e73119c3ef753ad275f9fd923b52732efa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Nov 2014 22:30:41 -0200 Subject: [PATCH 388/890] PEP8 --- social/backends/facebook.py | 2 +- social/backends/flickr.py | 2 +- social/backends/google.py | 2 +- social/backends/persona.py | 2 +- social/backends/vk.py | 4 ++-- social/backends/weibo.py | 2 +- social/pipeline/utils.py | 2 +- social/strategies/base.py | 2 -- social/tests/backends/legacy.py | 5 ++--- social/tests/backends/test_vk.py | 2 +- social/tests/pipeline.py | 2 ++ 11 files changed, 13 insertions(+), 14 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 0da7223e9..f318169db 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -129,7 +129,7 @@ def auth_complete(self, *args, **kwargs): if 'signed_request' in self.data: key, secret = self.get_key_and_secret() response = self.load_signed_request(self.data['signed_request']) - if not 'user_id' in response and not 'oauth_token' in response: + if 'user_id' not in response and 'oauth_token' not in response: raise AuthException(self) if response is not None: diff --git a/social/backends/flickr.py b/social/backends/flickr.py index 453ee69dd..b688beb25 100644 --- a/social/backends/flickr.py +++ b/social/backends/flickr.py @@ -38,6 +38,6 @@ def user_data(self, access_token, *args, **kwargs): def auth_extra_arguments(self): params = super(FlickrOAuth, self).auth_extra_arguments() or {} - if not 'perms' in params: + if 'perms' not in params: params['perms'] = 'read' return params diff --git a/social/backends/google.py b/social/backends/google.py index e16ad04b9..253615269 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -136,7 +136,7 @@ def auth_complete_params(self, state=None): return params def auth_complete(self, *args, **kwargs): - if 'access_token' in self.data and not 'code' in self.data: + if 'access_token' in self.data and 'code' not in self.data: raise AuthMissingParameter(self, 'access_token or code') # Token won't be available in plain server-side workflow diff --git a/social/backends/persona.py b/social/backends/persona.py index 0bafa639c..060715ca6 100644 --- a/social/backends/persona.py +++ b/social/backends/persona.py @@ -35,7 +35,7 @@ def extra_data(self, user, uid, response, details): def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" - if not 'assertion' in self.data: + if 'assertion' not in self.data: raise AuthMissingParameter(self, 'assertion') response = self.get_json('https://browserid.org/verify', data={ diff --git a/social/backends/vk.py b/social/backends/vk.py index e72926853..1b567bf40 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -190,9 +190,9 @@ def vk_api(backend, method, data): """ # We need to perform server-side call if no access_token data['v'] = backend.setting('API_VERSION', '3.0') - if not 'access_token' in data: + if 'access_token' not in data: key, secret = backend.get_key_and_secret() - if not 'api_id' in data: + if 'api_id' not in data: data['api_id'] = key data['method'] = method diff --git a/social/backends/weibo.py b/social/backends/weibo.py index b779520fb..6082ae9d3 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -1,4 +1,4 @@ -#coding:utf8 +# coding:utf8 # author:hepochen@gmail.com https://github.com/hepochen """ Weibo OAuth2 backend, docs at: diff --git a/social/pipeline/utils.py b/social/pipeline/utils.py index 76b8e5d3a..b1713090a 100644 --- a/social/pipeline/utils.py +++ b/social/pipeline/utils.py @@ -22,7 +22,7 @@ def partial_to_session(strategy, next, backend, request=None, *args, **kwargs): 'uid': social.uid } or None } - + kwargs.update(clean_kwargs) # Clean any MergeDict data type from the values diff --git a/social/strategies/base.py b/social/strategies/base.py index e7e4d4d2d..f2273b972 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -2,8 +2,6 @@ import random import hashlib -import six - from social.utils import setting_name, module_member from social.store import OpenIdStore, OpenIdSessionWrapper from social.pipeline import DEFAULT_AUTH_PIPELINE, DEFAULT_DISCONNECT_PIPELINE diff --git a/social/tests/backends/legacy.py b/social/tests/backends/legacy.py index a39079d9b..3e0947d92 100644 --- a/social/tests/backends/legacy.py +++ b/social/tests/backends/legacy.py @@ -14,9 +14,8 @@ def setUp(self): super(BaseLegacyTest, self).setUp() self.strategy.set_settings({ 'SOCIAL_AUTH_{0}_FORM_URL'.format(self.name): - self.strategy.build_absolute_uri( - '/login/{0}'.format(self.backend.name) - ) + self.strategy.build_absolute_uri('/login/{0}'.format( + self.backend.name)) }) def extra_settings(self): diff --git a/social/tests/backends/test_vk.py b/social/tests/backends/test_vk.py index 1966ddd7a..5a2a9aca3 100644 --- a/social/tests/backends/test_vk.py +++ b/social/tests/backends/test_vk.py @@ -1,4 +1,4 @@ -#coding: utf-8 +# coding: utf-8 from __future__ import unicode_literals import json diff --git a/social/tests/pipeline.py b/social/tests/pipeline.py index 133fc3c77..b52fdba21 100644 --- a/social/tests/pipeline.py +++ b/social/tests/pipeline.py @@ -27,6 +27,7 @@ def set_slug(strategy, user, *args, **kwargs): def remove_user(strategy, user, *args, **kwargs): return {'user': None} + @partial def set_user_from_kwargs(strategy, *args, **kwargs): if strategy.session_get('attribute'): @@ -34,6 +35,7 @@ def set_user_from_kwargs(strategy, *args, **kwargs): else: return strategy.redirect(strategy.build_absolute_uri('/attribute')) + @partial def set_user_from_args(strategy, user, *args, **kwargs): if strategy.session_get('attribute'): From 4fbb525a5564691a7b2bfa49ffa585f6f21f5d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Nov 2014 22:50:44 -0200 Subject: [PATCH 389/890] PEP8 + basic docs. Refs #412 --- docs/backends/index.rst | 1 + docs/backends/salesforce.rst | 44 +++++++++++++++++++++++++++++++++++ social/backends/salesforce.py | 9 ++++--- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 docs/backends/salesforce.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 74b5b5dae..e7d4c412f 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -96,6 +96,7 @@ Social backends readability reddit runkeeper + salesforce shopify skyrock soundcloud diff --git a/docs/backends/salesforce.rst b/docs/backends/salesforce.rst new file mode 100644 index 000000000..9b440a327 --- /dev/null +++ b/docs/backends/salesforce.rst @@ -0,0 +1,44 @@ +Salesforce +========== + +Salesforce uses OAuth v2 for Authentication, check the `official docs`_. + +- Create an app following the steps in the `Defining Connected Apps`_ docs. + +- Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_SALESFORCE_OAUTH2_KEY = '' + SOCIAL_AUTH_SALESFORCE_OAUTH2_SECRET = '' + +- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.salesforce.SalesforceOAuth2', + ... + ) + +- Then you can start using ``{% url social:begin 'salesforce-oauth2' %}`` in + your templates + + +If using the sandbox mode: + +- Fill these settings instead:: + + SOCIAL_AUTH_SALESFORCE_OAUTH2_SANDBOX_KEY = '' + SOCIAL_AUTH_SALESFORCE_OAUTH2_SANDBOX_SECRET = '' + +- And this backend:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.salesforce.SalesforceOAuth2Sandbox', + ... + ) + +- Then you can start using ``{% url social:begin 'salesforce-oauth2-sandbox' %}`` + in your templates + +.. _official docs: https://www.salesforce.com/us/developer/docs/api_rest/Content/intro_understanding_web_server_oauth_flow.htm +.. _Defining Connected Apps: https://www.salesforce.com/us/developer/docs/api_rest/Content/intro_defining_remote_access_applications.htm diff --git a/social/backends/salesforce.py b/social/backends/salesforce.py index 5ca77e21b..ccaca6302 100644 --- a/social/backends/salesforce.py +++ b/social/backends/salesforce.py @@ -5,7 +5,8 @@ class SalesforceOAuth2(BaseOAuth2): """Salesforce OAuth2 authentication backend""" name = 'salesforce-oauth2' - AUTHORIZATION_URL = 'https://login.salesforce.com/services/oauth2/authorize' + AUTHORIZATION_URL = \ + 'https://login.salesforce.com/services/oauth2/authorize' ACCESS_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token' REVOKE_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/revoke' ACCESS_TOKEN_METHOD = 'POST' @@ -17,7 +18,7 @@ class SalesforceOAuth2(BaseOAuth2): ('issued_at', 'issued_at'), ('signature', 'signature'), ('refresh_token', 'refresh_token'), - ] + ] def get_user_details(self, response): """Return user details from a Salesforce account""" @@ -32,9 +33,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" user_id_url = kwargs.get('response').get('id') - url = user_id_url + '?' + urlencode({ - 'access_token': access_token - }) + url = user_id_url + '?' + urlencode({'access_token': access_token}) try: return self.get_json(url) except ValueError: From 73c85f804a25ce17bcd47628dd1fb7c1387965fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Nov 2014 23:06:52 -0200 Subject: [PATCH 390/890] PEP8 --- social/storage/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/social/storage/base.py b/social/storage/base.py index d32a054cb..4520a3d98 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -188,9 +188,8 @@ def oids(cls, server_url, handle=None): kwargs = {'server_url': server_url} if handle is not None: kwargs['handle'] = handle - return sorted([ - (assoc.id, cls.openid_association(assoc)) - for assoc in cls.get(**kwargs) + return sorted([(assoc.id, cls.openid_association(assoc)) + for assoc in cls.get(**kwargs) ], key=lambda x: x[1].issued, reverse=True) @classmethod From aad06edaa8b0fba3ee3447609e8dad2284b9a15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Nov 2014 23:15:18 -0200 Subject: [PATCH 391/890] Rename tokens to access_token. Refs #430 --- social/storage/base.py | 9 ++++++++- social/tests/backends/test_dummy.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/social/storage/base.py b/social/storage/base.py index 4520a3d98..fe246e940 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -3,6 +3,8 @@ import time import base64 import uuid +import warnings + from datetime import datetime, timedelta import six @@ -34,10 +36,15 @@ def get_backend_instance(self, strategy=None): return Backend(strategy=strategy) @property - def tokens(self): + def access_token(self): """Return access_token stored in extra_data or None""" return self.extra_data.get('access_token') + @property + def tokens(self): + warnings.warn('tokens is deprecated, use access_token instead') + return self.access_token + def refresh_token(self, strategy, *args, **kwargs): token = self.extra_data.get('refresh_token') or \ self.extra_data.get('access_token') diff --git a/social/tests/backends/test_dummy.py b/social/tests/backends/test_dummy.py index 4ceab7558..53ca25b67 100644 --- a/social/tests/backends/test_dummy.py +++ b/social/tests/backends/test_dummy.py @@ -64,7 +64,7 @@ def test_partial_pipeline(self): def test_tokens(self): user = self.do_login() - self.assertEqual(user.social[0].tokens, 'foobar') + self.assertEqual(user.social[0].access_token, 'foobar') def test_revoke_token(self): self.strategy.set_settings({ From eaa3f567c08469f0b441467662a8e323c364808f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Nov 2014 23:30:56 -0200 Subject: [PATCH 392/890] Set no-cache on views --- social/apps/django_app/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 9123190fa..5ba1d59a0 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt, csrf_protect from django.views.decorators.http import require_POST +from django.views.decorators.cache import never_cache from social.utils import setting_name from social.actions import do_auth, do_complete, do_disconnect @@ -12,11 +13,13 @@ NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'social' +@never_cache @psa('{0}:complete'.format(NAMESPACE)) def auth(request, backend): return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME) +@never_cache @csrf_exempt @psa('{0}:complete'.format(NAMESPACE)) def complete(request, backend, *args, **kwargs): @@ -25,6 +28,7 @@ def complete(request, backend, *args, **kwargs): redirect_name=REDIRECT_FIELD_NAME, *args, **kwargs) +@never_cache @login_required @psa() @require_POST From f6dd9d004a53769a4109945f9507039f30ec666d Mon Sep 17 00:00:00 2001 From: John Lynn Date: Sat, 1 Nov 2014 18:41:24 -0700 Subject: [PATCH 393/890] Fix typo for AUTH_USER_MODEL --- social/apps/django_app/default/migrations/0001_initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index fdbad18ac..5c03be0f0 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -8,7 +8,7 @@ from social.utils import setting_name user_model = getattr(settings, setting_name('USER_MODEL'), None) or \ - getattr(settings, 'AUTH_USER_MODE', None) or \ + getattr(settings, 'AUTH_USER_MODEL', None) or \ 'auth.User' class Migration(migrations.Migration): From dc8aca86590f1568acac90eb2c1f62dcafe618cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 1 Nov 2014 23:51:45 -0200 Subject: [PATCH 394/890] PEP8 --- social/apps/django_app/default/migrations/0001_initial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 5c03be0f0..309f5db2e 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -11,6 +11,7 @@ getattr(settings, 'AUTH_USER_MODEL', None) or \ 'auth.User' + class Migration(migrations.Migration): dependencies = [ From b0f68dfc592f008edd3de04370cab5f2ac2a51ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 2 Nov 2014 00:01:26 -0200 Subject: [PATCH 395/890] Update changelog. Refs #421 --- Changelog | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/Changelog b/Changelog index 748655066..d937b10dc 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,156 @@ +2014-11-01 HEAD (unreleased) +============================ + + * 2014-11-01 Matías Aguirre + PEP8 + + * 2014-11-01 John Lynn + Fix typo for AUTH_USER_MODEL + + * 2014-11-01 Matías Aguirre + Set no-cache on views + + * 2014-11-01 Matías Aguirre + Rename tokens to access_token. Refs #430 + + * 2014-11-01 Matías Aguirre + PEP8 + + * 2014-11-01 Matías Aguirre + PEP8 + basic docs. Refs #412 + + * 2014-11-01 Matías Aguirre + PEP8 + + * 2014-10-30 Matías Aguirre + Link missing doc + + * 2014-10-30 Matías Aguirre + Fix use case snippet + + * 2014-10-29 Alex Parij + Update base.py + + * 2014-10-29 Mitchel Humpherys + use correct tense for `to meet' + + * 2014-10-26 John Lynn + Fix custom user model migrations for Django 1.7 + + * 2014-10-23 Matías Aguirre + Pick github primary email first. Fixes #413 + + * 2014-10-16 Christopher Grebs + Fix migration issue on python 3 + + * 2014-10-12 SilentSokolov + Fix does not match the number of arguments (for vk and ok backend) + + * 2014-10-08 Michal Karzyński + Salesforce OAuth2 support + + * 2014-10-07 Matías Aguirre + PEP8 and module rename + + * 2014-10-07 Matías Aguirre + Travisci update + + * 2014-10-07 Matías Aguirre + Enable pypy and replace sugar with unittest2. Refs #410 + + * 2014-10-07 Omer Katz + Added Python 3.4 and PyPy to the build matrix. + + * 2014-10-04 micahhausler + Added Django 1.7 App Config + + * 2014-10-02 micahhausler + Switched list_display order for UserSocialAuth + + * 2014-10-02 micahhausler + Added string method to UserSocialAuth model + + * 2014-10-03 Daniel Holmes + Use new GoogleOAuth2 Spec + + * 2014-10-01 Laban + Incorrect import path for db model + + * 2014-09-30 Matías Aguirre + PEP8 + + * 2014-09-30 Matías Aguirre + Convert docstring to comments, PEP8 + + * 2014-09-29 Lee Jaeyoung + Apply more detailed address for kakao + + * 2014-09-29 Lee Jaeyoung + Add Kakao to README.rst + + * 2014-09-28 David Zerrenner + Added some legal stuff + + * 2014-09-27 Aarni Koskela + Recreate migration with Django 1.7 final and re-PEP8. + + * 2014-09-26 Matías Aguirre + Use getattr to get current backend from request + + * 2014-09-25 Matías Aguirre + Doc about custom url namespace. Refs #399 + + * 2014-09-25 Matías Aguirre + Configurable django views namespace. Refs #399 + + * 2014-09-25 Matías Aguirre + PEP8 and more + + * 2014-09-24 Vera Mazhuga + master add SCOPE_SEPARATOR to DisqusOAuth2 + + * 2014-09-24 dzerrenner + added a backend for Battle.net Oauth2 auth + + * 2014-09-23 Matías Aguirre + PEP8 + + * 2014-09-23 Matías Aguirre + PEP8 + + * 2014-09-23 David Henderson + Removed prefix, added example of details object. + + * 2014-09-23 David Henderson + No good reason to skip a class + + * 2014-09-21 Matías Aguirre + Don't update a setting value. Refs #378. Refs #377 + + * 2014-09-21 Tim Savage + Update installing.rst + + * 2014-09-21 Tim Savage + Update README.rst + + * 2014-09-18 Matías Aguirre + Print arguments in the debug pipeline + + * 2014-09-16 Stefan Kröner + Allow more Trello settings + + * 2014-09-12 Matías Aguirre + Add debug pipeline to example app + + * 2014-09-12 Matías Aguirre + Update snippet + + * 2014-09-12 David Henderson + Updated to use latest api wrapper + + * 2014-09-11 Matías Aguirre + Flag dev version + 2014-09-11 v0.2.1 ================= @@ -13,12 +166,29 @@ * 2014-09-11 Matías Aguirre Flag dev version +2014-09-11 v0.2.0 +================= + * 2014-09-11 Matías Aguirre v0.2.0 * 2014-09-11 Matías Aguirre Restore @strategy decorator with warning message + * 2014-09-09 Tsung Hung + updated the docs to add migrations for 1.7 while updated a constant so the + warning message does not appear when running command line + + * 2014-09-09 Tsung Hung + updated the docs to add migrations for 1.7 while updated a constant so the + warning message does not appear when running command line + + * 2014-09-07 Amol Kher + Jawbone needs params instead of data as requests + + * 2014-09-07 Caio Ariede + Support for MineID.org + * 2014-09-03 Matías Aguirre Added commets detailing pipeline functionality. Refs #361 @@ -131,6 +301,15 @@ * 2014-07-24 Matt Luongo Support South and Django 1.7+ migrations. + * 2014-07-23 Mike Anderson + remove debugger + + * 2014-07-23 Mike Anderson + tests for two failing cases, include all kwargs in partial pipeline session + + * 2014-07-22 Mike Anderson + Don't overwrite clean_kwargs with kwargs + * 2014-07-19 Matías Aguirre Github for teams backend. Refs #329 @@ -174,6 +353,16 @@ * 2014-06-30 Matías Aguirre Tox runner with pyenv support + * 2014-06-26 David Henderson + Reinstated get_user_id override - so that we can pull from the details + rather than the response + + * 2014-06-25 Antony Seedhouse + Update django_orm.py + + * 2014-06-25 Antony Seedhouse + Update django_orm.py + * 2014-06-24 Martey Dodoo Update link to Django example in documentation. From 198c1614735c5ee0b84b4636abc874f66ee62c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 2 Nov 2014 00:06:01 -0200 Subject: [PATCH 396/890] Pass request to pyramid strategy. Refs #390 --- social/apps/pyramid_app/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/social/apps/pyramid_app/utils.py b/social/apps/pyramid_app/utils.py index 832bc5b8a..d0c3adde1 100644 --- a/social/apps/pyramid_app/utils.py +++ b/social/apps/pyramid_app/utils.py @@ -21,8 +21,12 @@ def get_helper(name): return settings.get(setting_name(name), DEFAULTS.get(name, None)) -def load_strategy(): - return get_strategy(get_helper('STRATEGY'), get_helper('STORAGE')) +def load_strategy(request): + return get_strategy( + get_helper('STRATEGY'), + get_helper('STORAGE'), + request + ) def load_backend(strategy, name, redirect_uri): @@ -43,7 +47,7 @@ def wrapper(request, *args, **kwargs): if uri and not uri.startswith('/'): uri = request.route_url(uri, backend=backend) - request.strategy = load_strategy() + request.strategy = load_strategy(request) request.backend = load_backend(request.strategy, backend, uri) return func(request, *args, **kwargs) return wrapper From 3b9910339887d0aeaedfb3e22752b499beb22190 Mon Sep 17 00:00:00 2001 From: Miguel Paolino Date: Fri, 7 Nov 2014 16:42:37 -0200 Subject: [PATCH 397/890] Added Zotero OAuth1 backend --- docs/backends/zotero.rst | 25 +++++++++++++++++++++++++ social/backends/zotero.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 docs/backends/zotero.rst create mode 100644 social/backends/zotero.py diff --git a/docs/backends/zotero.rst b/docs/backends/zotero.rst new file mode 100644 index 000000000..0a8330e37 --- /dev/null +++ b/docs/backends/zotero.rst @@ -0,0 +1,25 @@ +Zotero +====== + +Zotero implements OAuth1 as their authentication mechanism for their Web API v3. + + +1. Go to the `Zotero app registration page`_ to register your application. + +2. Fill the **Client ID** and **Client Secret** in your project settings:: + + SOCIAL_AUTH_ZOTERO_KEY = '...' + SOCIAL_AUTH_ZOTERO_SECRET = '...' + +3. Enable the backend:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.amazon.Zotero', + ... + ) + +Further documentation at `Zotero Web API v3 page`_. + +.. _Zotero app registration page: https://www.zotero.org/oauth/apps +.. _Zotero Web API v3 page: https://www.zotero.org/support/dev/web_api/v3/start diff --git a/social/backends/zotero.py b/social/backends/zotero.py new file mode 100644 index 000000000..427e74b4b --- /dev/null +++ b/social/backends/zotero.py @@ -0,0 +1,31 @@ +""" +Zotero OAuth1 backends, docs at: + http://psa.matiasaguirre.net/docs/backends/zotero.html +""" +from requests import HTTPError + +from social.backends.oauth import BaseOAuth2, BaseOAuth1 +from social.exceptions import AuthMissingParameter, AuthCanceled +import ipdb + + +class ZoteroOAuth(BaseOAuth1): + + """Zotero OAuth authorization mechanism""" + name = 'zotero' + AUTHORIZATION_URL = 'https://www.zotero.org/oauth/authorize' + REQUEST_TOKEN_URL = 'https://www.zotero.org/oauth/request' + ACCESS_TOKEN_URL = 'https://www.zotero.org/oauth/access' + + def get_user_id(self, details, response): + """ + Return user unique id provided by service. For Ubuntu One + the nickname should be original. + """ + return details['userID'] + + def get_user_details(self, response): + """Return user details from Zotero API account""" + access_token = response.get('access_token', dict()) + return {'username': access_token.get('username', ''), + 'userID': access_token.get('userID', '')} From 9e7373d90e019b4db1f740de0f63437b817b2ddf Mon Sep 17 00:00:00 2001 From: Miguel Paolino Date: Fri, 7 Nov 2014 16:47:29 -0200 Subject: [PATCH 398/890] Fixed doc line --- docs/backends/zotero.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/zotero.rst b/docs/backends/zotero.rst index 0a8330e37..19da98221 100644 --- a/docs/backends/zotero.rst +++ b/docs/backends/zotero.rst @@ -15,7 +15,7 @@ Zotero implements OAuth1 as their authentication mechanism for their Web API v3. SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( ... - 'social.backends.amazon.Zotero', + 'social.backends.zotero.ZoteroOAuth', ... ) From 9411c487b9c362060dec0f260f1ad40fcf4883ec Mon Sep 17 00:00:00 2001 From: Miguel Paolino Date: Fri, 7 Nov 2014 16:59:21 -0200 Subject: [PATCH 399/890] Udpated README to include the Zotero backend mention --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index fadf0cb5f..999ae6cf4 100644 --- a/README.rst +++ b/README.rst @@ -119,6 +119,7 @@ or current ones extended): * Yahoo_ OpenId and OAuth1 * Yammer_ OAuth2 * Yandex_ OAuth1, OAuth2 and OpenId + * Zotero_ OAuth1 User data @@ -300,3 +301,4 @@ check `django-social-auth LICENSE`_ for details: .. _six: http://pythonhosted.org/six/ .. _requests: http://docs.python-requests.org/en/latest/ .. _PixelPin: http://pixelpin.co.uk +.. _Zotero: http://www.zotero.org/ From cb2792cd043f1f4922d85ec72cb2de5532dc1cea Mon Sep 17 00:00:00 2001 From: Miguel Paolino Date: Fri, 7 Nov 2014 18:09:00 -0200 Subject: [PATCH 400/890] Added zotero test, work in progress --- social/tests/backends/test_zotero.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 social/tests/backends/test_zotero.py diff --git a/social/tests/backends/test_zotero.py b/social/tests/backends/test_zotero.py new file mode 100644 index 000000000..d9b837d2e --- /dev/null +++ b/social/tests/backends/test_zotero.py @@ -0,0 +1,28 @@ +import json + +from social.p3 import urlencode +from social.tests.backends.oauth import OAuth1Test + + +class ZoteroOAuth1Test(OAuth1Test): + backend_path = 'social.backends.zotero.ZoteroOAuth' + expected_username = 'FooBar' + + + access_token_body = json.dumps({ + 'access_token': {u'oauth_token': u'foobar', + u'oauth_token_secret': u'rodgsNDK4hLJU1504Atk131G', + u'userID': u'123456_abcdef', + u'username': u'anyusername'}}) + + request_token_body = urlencode({ + 'oauth_token_secret': 'foobar-secret', + 'oauth_token': 'foobar', + 'oauth_callback_confirmed': 'true' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 5c751c0c1625b7a7da03dca31311d1a00f00de37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 11 Nov 2014 09:26:25 -0200 Subject: [PATCH 401/890] Example on how to re-prompt a google user to get the refresh_token --- docs/use_cases.rst | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 9fa95c69b..3ba1cadb1 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -258,5 +258,43 @@ It needs to be somewhere before create_user because the partial will change the username according to the users choice. +Re-prompt Google OAuth2 users to refresh the ``refresh_token`` +-------------------------------------------------------------- + +A ``refresh_token`` also expire, a ``refresh_token`` can be lost, but they can +also be refreshed (or re-fetched) if you ask to Google the right way. In order +to do so, set this setting:: + + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = { + 'access_type': 'offline', + 'approval_prompt': 'auto' + } + +Then link the users to ``/login/google-oauth2?approval_prompt=force``. If you +want to refresh the ``refresh_token`` only on those users that don't have it, +do it with a pipeline function:: + + def redirect_if_no_refresh_token(backend, response, social, *args, **kwargs): + if backend.name == 'google-oauth2' and social and \ + response.get('refresh_token') is None and \ + social.extra_data.get('refresh_token') is None: + return redirect('/login/google-oauth2?approval_prompt=force') + +Set this pipeline after ``social_user``:: + + SOCIAL_AUTH_PIPELINE = ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.auth_allowed', + 'social.pipeline.social_auth.social_user', + 'import.path.to.redirect_if_no_refresh_token', + 'social.pipeline.user.get_username', + 'social.pipeline.user.create_user', + 'social.pipeline.social_auth.associate_user', + 'social.pipeline.social_auth.load_extra_data', + 'social.pipeline.user.user_details', + ) + + .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list From eaae97ad168fe1ba3e679257571a043ffc015a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 14 Nov 2014 21:26:42 -0200 Subject: [PATCH 402/890] Remove x flag from .py file --- social/backends/pushbullet.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 social/backends/pushbullet.py diff --git a/social/backends/pushbullet.py b/social/backends/pushbullet.py old mode 100755 new mode 100644 From 53ead5c4f66a79d148f2adfa3f54835c4436ab8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Nov 2014 20:04:03 -0200 Subject: [PATCH 403/890] Fix docs. Refs #436 --- docs/pipeline.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 6369779ee..4e5b9acdf 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -185,8 +185,9 @@ email address you can get it from the session under the key ``email_validation_a In order to send the validation python-social-auth_ needs a function that will take care of it, this function is defined by the developer with the setting ``SOCIAL_AUTH_EMAIL_VALIDATION_FUNCTION``. It should be an import path. This -function should take two arguments ``strategy`` and ``code``. ``code`` is -a model instance used to validate the email address, it contains three fields: +function should take three arguments ``strategy``, ``backend`` and ``code``. +``code`` is a model instance used to validate the email address, it contains +three fields: ``code = '...'`` Holds an ``uuid.uuid4()`` value and it's the code used to identify the From 15e366f4531dac74b8fb35718f1dd13a316915c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 15 Nov 2014 20:22:10 -0200 Subject: [PATCH 404/890] Avoid override of custom-usernames fields with plain generated username. Refs #435 --- social/storage/django_orm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 02d233ab4..7aaf9821a 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -54,8 +54,9 @@ def get_username(cls, user): @classmethod def create_user(cls, *args, **kwargs): - if 'username' in kwargs: - kwargs[cls.username_field()] = kwargs.pop('username') + username_field = cls.username_field() + if 'username' in kwargs and username_field not in kwargs: + kwargs[username_field] = kwargs.pop('username') return cls.user_model().objects.create_user(*args, **kwargs) @classmethod From 751853dd67ba14f8163721d7b28c24d3c227c77a Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Sun, 16 Nov 2014 16:25:15 +0100 Subject: [PATCH 405/890] Basic Khan Academy support --- README.rst | 1 + social/backends/khanacademy.py | 31 +++++++++++++++++++++++ social/tests/backends/test_khanacademy.py | 30 ++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 social/backends/khanacademy.py create mode 100644 social/tests/backends/test_khanacademy.py diff --git a/README.rst b/README.rst index fadf0cb5f..71e8922bb 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,7 @@ or current ones extended): * Instagram_ OAuth2 * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication * Kakao_ OAuth2 https://developer.kakao.com + * `Khan Academy`_ OAuth1 https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py new file mode 100644 index 000000000..1c5dd038e --- /dev/null +++ b/social/backends/khanacademy.py @@ -0,0 +1,31 @@ +""" +Khan Academy OAuth backend, docs at: + http://psa.matiasaguirre.net/docs/backends/facebook.html +""" + +from social.backends.oauth import BaseOAuth1 + + +class KhanAcademyOAuth1(BaseOAuth1): + name = 'khanacademy-oauth' + ID_KEY = 'user_id' + AUTHORIZATION_URL = 'https://www.khanacademy.org/api/auth' + REQUEST_TOKEN_URL = 'https://www.khanacademy.org/api/auth/request_token' + ACCESS_TOKEN_URL = 'https://www.khanacademy.org/api/auth/access_token' + REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' + + def get_user_details(self, response): + """Return user details from Facebook account""" + return { + 'username': response.get('key_email'), + 'email': response.get('key_email'), + 'fullname': '', + 'first_name': '', + 'last_name': '', + 'user_id': response.get('user_id') + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://www.khanacademy.org/api/v1/user', + auth=self.oauth_auth(access_token)) diff --git a/social/tests/backends/test_khanacademy.py b/social/tests/backends/test_khanacademy.py new file mode 100644 index 000000000..65481a02c --- /dev/null +++ b/social/tests/backends/test_khanacademy.py @@ -0,0 +1,30 @@ +import json + +from social.p3 import urlencode + +from social.tests.backends.oauth import OAuth1Test + + +class KhanAcademyOAuth2Test(OAuth1Test): + backend_path = 'social.backends.khanacademy.KhanAcademyOAuth1' + user_data_url = 'https://www.khanacademy.org/api/v1/user' + expected_username = 'foo@bar.com' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + request_token_body = urlencode({ + 'oauth_token_secret': 'foobar-secret', + 'oauth_token': 'foobar', + 'oauth_callback_confirmed': 'true' + }) + user_data_body = json.dumps({ + "key_email": "foo@bar.com", + "user_id": "http://googleid.khanacademy.org/11111111111111", + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From da6d2353adf1cb390af0cede259827d1a057bd3c Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Sun, 16 Nov 2014 16:53:02 +0100 Subject: [PATCH 406/890] Fix readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 71e8922bb..37dd57671 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,7 @@ or current ones extended): * Instagram_ OAuth2 * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication * Kakao_ OAuth2 https://developer.kakao.com - * `Khan Academy`_ OAuth1 https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication + * `Khan Academy`_ https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication OAuth1 * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId From d2a93b3a6f592a2cceff7d94ef3611d515f9a8cb Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Sun, 16 Nov 2014 23:07:45 +0100 Subject: [PATCH 407/890] Fix backend name formatting --- README.rst | 3 ++- social/backends/khanacademy.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 37dd57671..2e03140fc 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,7 @@ or current ones extended): * Instagram_ OAuth2 * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication * Kakao_ OAuth2 https://developer.kakao.com - * `Khan Academy`_ https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication OAuth1 + * `Khan Academy`_ OAuth1 * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId @@ -249,6 +249,7 @@ check `django-social-auth LICENSE`_ for details: .. _Linkedin: https://www.linkedin.com .. _Live: https://live.com .. _Livejournal: http://livejournal.com +.. _Khan Academy: https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication .. _Mailru: https://mail.ru .. _MapMyFitness: http://www.mapmyfitness.com/ .. _Mixcloud: https://www.mixcloud.com diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py index 1c5dd038e..17b390c00 100644 --- a/social/backends/khanacademy.py +++ b/social/backends/khanacademy.py @@ -7,7 +7,7 @@ class KhanAcademyOAuth1(BaseOAuth1): - name = 'khanacademy-oauth' + name = 'khanacademy-oauth1' ID_KEY = 'user_id' AUTHORIZATION_URL = 'https://www.khanacademy.org/api/auth' REQUEST_TOKEN_URL = 'https://www.khanacademy.org/api/auth/request_token' From f73ec732ae6153f680e14038b16be511ad2359cb Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Tue, 18 Nov 2014 18:31:33 +0100 Subject: [PATCH 408/890] Struggling with Khan Academy again... Conflicts: social/backends/khanacademy.py --- social/backends/khanacademy.py | 47 ++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py index 17b390c00..86fcd7d3a 100644 --- a/social/backends/khanacademy.py +++ b/social/backends/khanacademy.py @@ -3,17 +3,60 @@ http://psa.matiasaguirre.net/docs/backends/facebook.html """ +from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_QUERY from social.backends.oauth import BaseOAuth1 class KhanAcademyOAuth1(BaseOAuth1): + """ + Class used for autorising with Khan Academy. + + Flow of Khan Academy is a bit different than most OAuth 1.0 and consinsts + of the following steps: + 1. Create signed params to attach to the REQUEST_TOKEN_URL + 2. Redirect user to the REQUEST_TOKEN_URL that will respond with + oauth_secret, oauth_token, oauth_verifier that should be used with + ACCESS_TOKEN_URL + 3. Go to ACCESS_TOKEN_URL and grab oauth_token_secret. + + Note that we don't use the AUTHORIZATION_URL. + + AUTHORIZATION_URL requires the following arguments: + oauth_consumer_key - Your app's consumer key + oauth_nonce - Random 64-bit, unsigned number encoded as an ASCII string + in decimal format. The nonce/timestamp pair should always be unique. + oauth_version - OAuth version used by your app. Must be "1.0" for now. + oauth_signature - String generated using the referenced signature method. + oauth_signature_method - Signature algorithm (currently only support + "HMAC-SHA1") + oauth_timestamp - Integer representing the time the request is sent. + The timestamp should be expressed in number of seconds + after January 1, 1970 00:00:00 GMT. + oauth_callback (optional) - URL to redirect to after request token is + received and authorized by the user's chosen identity provider. + """ name = 'khanacademy-oauth1' ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://www.khanacademy.org/api/auth' - REQUEST_TOKEN_URL = 'https://www.khanacademy.org/api/auth/request_token' + REQUEST_TOKEN_URL = 'http://www.khanacademy.org/api/auth/request_token' ACCESS_TOKEN_URL = 'https://www.khanacademy.org/api/auth/access_token' REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' + def oauth_authorization_request(self, token): + """Generate OAuth request to authorize token.""" + key, secret = self.get_key_and_secret() + state = self.get_or_create_state() + auth_client = Client( + key, secret, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_QUERY, + callback_uri=self.get_redirect_uri(state) + ) + url, headers, body = auth_client.sign(self.REQUEST_TOKEN_URL) + return url + + def get_unauthorized_token(self): + return self.strategy.request_data() + def get_user_details(self, response): """Return user details from Facebook account""" return { From bebef4ef7e1ada4bb029334a3e061e1c8a3fa283 Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Tue, 18 Nov 2014 23:23:05 +0100 Subject: [PATCH 409/890] Khan Academy oauth support now fully working --- social/backends/khanacademy.py | 91 ++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 21 deletions(-) diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py index 86fcd7d3a..0e76cc668 100644 --- a/social/backends/khanacademy.py +++ b/social/backends/khanacademy.py @@ -2,12 +2,75 @@ Khan Academy OAuth backend, docs at: http://psa.matiasaguirre.net/docs/backends/facebook.html """ +import six + +from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_TYPE_QUERY +from requests_oauthlib import OAuth1 -from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_QUERY from social.backends.oauth import BaseOAuth1 +from social.p3 import urlencode + + +class BrowserBasedOAuth1(BaseOAuth1): + """Browser based mechanism OAuth authentication, fill the needed + parameters to communicate properly with authentication service. + + REQUEST_TOKEN_URL Request token URL (opened in web browser) + ACCESS_TOKEN_URL Access token URL + """ + REQUEST_TOKEN_URL = '' + OAUTH_TOKEN_PARAMETER_NAME = 'oauth_token' + REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' + ACCESS_TOKEN_URL = '' + + def auth_url(self): + """Return redirect url""" + return self.unauthorized_token_request() + + def get_unauthorized_token(self): + return self.strategy.request_data() + + def unauthorized_token_request(self): + """Return request for unauthorized token (first stage)""" + + params = self.request_token_extra_arguments() + params.update(self.get_scope_argument()) + key, secret = self.get_key_and_secret() + # decoding='utf-8' produces errors with python-requests on Python3 + # since the final URL will be of type bytes + decoding = None if six.PY3 else 'utf-8' + state = self.get_or_create_state() + auth = OAuth1( + key, + secret, + callback_uri=self.get_redirect_uri(state), + decoding=decoding, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_QUERY + ) + url = self.REQUEST_TOKEN_URL + '?' + urlencode(params) + url, _, _ = auth.client.sign(url) + return url + def oauth_auth(self, token=None, oauth_verifier=None): + key, secret = self.get_key_and_secret() + oauth_verifier = oauth_verifier or self.data.get('oauth_verifier') + token = token or {} + # decoding='utf-8' produces errors with python-requests on Python3 + # since the final URL will be of type bytes + decoding = None if six.PY3 else 'utf-8' + state = self.get_or_create_state() + return OAuth1(key, secret, + resource_owner_key=token.get('oauth_token'), + resource_owner_secret=token.get('oauth_token_secret'), + callback_uri=self.get_redirect_uri(state), + verifier=oauth_verifier, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_QUERY, + decoding=decoding) -class KhanAcademyOAuth1(BaseOAuth1): + +class KhanAcademyOAuth1(BrowserBasedOAuth1): """ Class used for autorising with Khan Academy. @@ -40,25 +103,10 @@ class KhanAcademyOAuth1(BaseOAuth1): REQUEST_TOKEN_URL = 'http://www.khanacademy.org/api/auth/request_token' ACCESS_TOKEN_URL = 'https://www.khanacademy.org/api/auth/access_token' REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' - - def oauth_authorization_request(self, token): - """Generate OAuth request to authorize token.""" - key, secret = self.get_key_and_secret() - state = self.get_or_create_state() - auth_client = Client( - key, secret, - signature_method=SIGNATURE_HMAC, - signature_type=SIGNATURE_TYPE_QUERY, - callback_uri=self.get_redirect_uri(state) - ) - url, headers, body = auth_client.sign(self.REQUEST_TOKEN_URL) - return url - - def get_unauthorized_token(self): - return self.strategy.request_data() + USER_DATA_URL = 'https://www.khanacademy.org/api/v1/user' def get_user_details(self, response): - """Return user details from Facebook account""" + """Return user details from Khan Academy account""" return { 'username': response.get('key_email'), 'email': response.get('key_email'), @@ -70,5 +118,6 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - return self.get_json('https://www.khanacademy.org/api/v1/user', - auth=self.oauth_auth(access_token)) + auth = self.oauth_auth(access_token) + url, _, _ = auth.client.sign(self.USER_DATA_URL) + return self.get_json(url) From d6038b450b5de4b9d9134399e68e0d65e10e2ca6 Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Tue, 18 Nov 2014 23:31:56 +0100 Subject: [PATCH 410/890] Fixed docs --- social/backends/khanacademy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py index 0e76cc668..48655865f 100644 --- a/social/backends/khanacademy.py +++ b/social/backends/khanacademy.py @@ -1,6 +1,6 @@ """ Khan Academy OAuth backend, docs at: - http://psa.matiasaguirre.net/docs/backends/facebook.html + https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication """ import six @@ -84,7 +84,7 @@ class KhanAcademyOAuth1(BrowserBasedOAuth1): Note that we don't use the AUTHORIZATION_URL. - AUTHORIZATION_URL requires the following arguments: + REQUEST_TOKEN_URL requires the following arguments: oauth_consumer_key - Your app's consumer key oauth_nonce - Random 64-bit, unsigned number encoded as an ASCII string in decimal format. The nonce/timestamp pair should always be unique. From 6099db4a326f061412e44daebf380d1ccbf56505 Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Tue, 18 Nov 2014 23:39:54 +0100 Subject: [PATCH 411/890] Changed test name --- social/tests/backends/test_khanacademy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/tests/backends/test_khanacademy.py b/social/tests/backends/test_khanacademy.py index 65481a02c..e7ee8633b 100644 --- a/social/tests/backends/test_khanacademy.py +++ b/social/tests/backends/test_khanacademy.py @@ -5,7 +5,7 @@ from social.tests.backends.oauth import OAuth1Test -class KhanAcademyOAuth2Test(OAuth1Test): +class KhanAcademyOAuth1Test(OAuth1Test): backend_path = 'social.backends.khanacademy.KhanAcademyOAuth1' user_data_url = 'https://www.khanacademy.org/api/v1/user' expected_username = 'foo@bar.com' From 4d70b23eb603c1d9753a7982bd7b3bab7cf18d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=C3=A1n=20Hayes?= Date: Wed, 19 Nov 2014 17:20:15 -0500 Subject: [PATCH 412/890] Added support for Django's User.EMAIL_FIELD. Useful for when an alternate User.EMAIL_FIELD is defined. --- social/storage/django_orm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 7aaf9821a..8d89e4fa0 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -70,7 +70,11 @@ def get_user(cls, pk=None, **kwargs): @classmethod def get_users_by_email(cls, email): - return cls.user_model().objects.filter(email__iexact=email) + user_model = cls.user_model() + + email_field = getattr(user_model, 'EMAIL_FIELD', 'email') + + return user_model.objects.filter(**{email_field+'__iexact': email}) @classmethod def get_social_auth(cls, provider, uid): From df3ff37f606e667069940732e5437ade4b70002c Mon Sep 17 00:00:00 2001 From: tschilling Date: Fri, 21 Nov 2014 09:08:04 -0500 Subject: [PATCH 413/890] Allow the pipeline to change the redirect url. Moves the popping of the redirect value from the session to after the pipe line executes. --- social/actions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/social/actions.py b/social/actions.py index 0001f0351..154c6e4ba 100644 --- a/social/actions.py +++ b/social/actions.py @@ -27,10 +27,7 @@ def do_auth(backend, redirect_name='next'): def do_complete(backend, login, user=None, redirect_name='next', *args, **kwargs): - # pop redirect value before the session is trashed on login() data = backend.strategy.request_data() - redirect_value = backend.strategy.session_get(redirect_name, '') or \ - data.get(redirect_name, '') is_authenticated = user_is_authenticated(user) user = is_authenticated and user or None @@ -42,6 +39,12 @@ def do_complete(backend, login, user=None, redirect_name='next', else: user = backend.complete(user=user, *args, **kwargs) + + # pop redirect value before the session is trashed on login(), but after + # the pipeline so that the pipeline can change the redirect if needed + redirect_value = backend.strategy.session_get(redirect_name, '') or \ + data.get(redirect_name, '') + user_model = backend.strategy.storage.user.user_model() if user and not isinstance(user, user_model): return user From e342d258764194c10ea9369d9bfc329b6bc8e7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 22 Nov 2014 13:07:22 -0200 Subject: [PATCH 414/890] PEP8 --- social/storage/django_orm.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 8d89e4fa0..8d9e67246 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -71,10 +71,8 @@ def get_user(cls, pk=None, **kwargs): @classmethod def get_users_by_email(cls, email): user_model = cls.user_model() - email_field = getattr(user_model, 'EMAIL_FIELD', 'email') - - return user_model.objects.filter(**{email_field+'__iexact': email}) + return user_model.objects.filter(**{email_field + '__iexact': email}) @classmethod def get_social_auth(cls, provider, uid): From 65b6d6a571070c24e56ddad1f94d40b318db806e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 22 Nov 2014 13:08:07 -0200 Subject: [PATCH 415/890] PEP8 --- social/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/social/actions.py b/social/actions.py index 154c6e4ba..8d18bfa3c 100644 --- a/social/actions.py +++ b/social/actions.py @@ -39,7 +39,6 @@ def do_complete(backend, login, user=None, redirect_name='next', else: user = backend.complete(user=user, *args, **kwargs) - # pop redirect value before the session is trashed on login(), but after # the pipeline so that the pipeline can change the redirect if needed redirect_value = backend.strategy.session_get(redirect_name, '') or \ From 02bed89211aa96c19c0e776f83d2d84679841f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 22 Nov 2014 13:25:50 -0200 Subject: [PATCH 416/890] Quick khan academy docs --- docs/backends/index.rst | 1 + docs/backends/khanacademy.rst | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 docs/backends/khanacademy.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index e7d4c412f..ae9afe54f 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -74,6 +74,7 @@ Social backends instagram jawbone kakao + khanacademy lastfm linkedin livejournal diff --git a/docs/backends/khanacademy.rst b/docs/backends/khanacademy.rst new file mode 100644 index 000000000..79e2f618c --- /dev/null +++ b/docs/backends/khanacademy.rst @@ -0,0 +1,25 @@ +Khan Academy +============ + +Khan Academy uses a variant of OAuth1 authentication flow. Check the API +details at `Khan Academy API Authentication`_. + +Follow this steps in order to use the backend: + +- Register a new application at `Khan Academy API Apps`_, + +- Fill **Consumer Key** and **Consumer Secret** values:: + + SOCIAL_AUTH_KHANACADEMY_OAUTH1_KEY = '' + SOCIAL_AUTH_KHANACADEMY_OAUTH1_SECRET = '' + +- Add the backend to ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.khanacademy.KhanAcademyOAuth1', + ... + ) + +.. _Khan Academy API Authentication: https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication +.. _Khan Academy API Apps: http://www.khanacademy.org/api-apps/register From 0e411aa2a78136c99ffd36fde232ceef7eb252e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 22 Nov 2014 21:52:36 -0200 Subject: [PATCH 417/890] Allow initial definition of protected attributes --- social/pipeline/user.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 9f20cb0ce..1e9971cbb 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -73,19 +73,20 @@ def user_details(strategy, details, user=None, *args, **kwargs): """Update user details using data from provider.""" if user: changed = False # flag to track changes - protected = strategy.setting('PROTECTED_USER_FIELDS', []) - keep = ('username', 'id', 'pk') + tuple(protected) + protected = ('username', 'id', 'pk') + \ + tuple(strategy.setting('PROTECTED_USER_FIELDS', [])) + # Update user model attributes with the new data sent by the current + # provider. Update on some attributes is disabled by default, for + # example username and id fields. It's also possible to disable update + # on fields defined in SOCIAL_AUTH_PROTECTED_FIELDS. for name, value in details.items(): - # do not update username, it was already generated - # do not update configured fields if user already existed - if name not in keep and hasattr(user, name): - if value and value != getattr(user, name, None): - try: - setattr(user, name, value) - changed = True - except AttributeError: - pass + if not hasattr(user, name): + continue + current_value = getattr(user, name, None) + if not current_value or name not in protected: + changed |= current_value != value + setattr(user, name, value) if changed: strategy.storage.user.changed(user) From cc5908675c95944b68aae038700c7ee901aff491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 23 Nov 2014 01:45:47 -0200 Subject: [PATCH 418/890] Simplify flask app initialization --- examples/flask_example/__init__.py | 15 ++-- examples/flask_example/manage.py | 8 +- examples/flask_example/models/user.py | 23 ++++-- examples/flask_example/settings.py | 9 +-- social/apps/flask_app/default/models.py | 103 ++++++++++++++---------- social/storage/sqlalchemy_orm.py | 3 +- 6 files changed, 92 insertions(+), 69 deletions(-) diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py index f791441f2..a5ebd77c8 100644 --- a/examples/flask_example/__init__.py +++ b/examples/flask_example/__init__.py @@ -1,7 +1,9 @@ import sys +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker + from flask import Flask, g -from flask.ext.sqlalchemy import SQLAlchemy from flask.ext import login sys.path.append('../..') @@ -20,9 +22,12 @@ pass # DB -db = SQLAlchemy(app) +engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI']) +Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) +db_session = scoped_session(Session) + app.register_blueprint(social_auth) -init_social(app, db) +init_social(app, db_session) login_manager = login.LoginManager() login_manager.login_view = 'main' @@ -49,12 +54,12 @@ def global_user(): @app.teardown_appcontext def commit_on_success(error=None): if error is None: - db.session.commit() + db_session.commit() @app.teardown_request def shutdown_session(exception=None): - db.session.remove() + db_session.remove() @app.context_processor diff --git a/examples/flask_example/manage.py b/examples/flask_example/manage.py index 9c6302fcd..a298e0022 100755 --- a/examples/flask_example/manage.py +++ b/examples/flask_example/manage.py @@ -5,14 +5,14 @@ sys.path.append('..') -from flask_example import app, db +from flask_example import app, db_session, engine manager = Manager(app) manager.add_command('runserver', Server()) manager.add_command('shell', Shell(make_context=lambda: { 'app': app, - 'db': db + 'db_session': db_session })) @@ -20,8 +20,8 @@ def syncdb(): from flask_example.models import user from social.apps.flask_app.default import models - db.drop_all() - db.create_all() + user.Base.metadata.create_all(engine) + models.PSABase.metadata.create_all(engine) if __name__ == '__main__': manager.run() diff --git a/examples/flask_example/models/user.py b/examples/flask_example/models/user.py index 32727f6c1..7cb70580e 100644 --- a/examples/flask_example/models/user.py +++ b/examples/flask_example/models/user.py @@ -1,16 +1,23 @@ +from sqlalchemy import Column, String, Integer, Boolean +from sqlalchemy.ext.declarative import declarative_base + from flask.ext.login import UserMixin -from flask_example import db +from flask_example import db_session + + +Base = declarative_base() +Base.query = db_session.query_property() -class User(db.Model, UserMixin): +class User(Base, UserMixin): __tablename__ = 'users' - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(200)) - password = db.Column(db.String(200), default='') - name = db.Column(db.String(100)) - email = db.Column(db.String(200)) - active = db.Column(db.Boolean, default=True) + id = Column(Integer, primary_key=True) + username = Column(String(200)) + password = Column(String(200), default='') + name = Column(String(100)) + email = Column(String(200)) + active = Column(Boolean, default=True) def is_active(self): return self.active diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 839e0098d..101f1b8f5 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -1,14 +1,9 @@ -from flask_example import app - - -app.debug = True +from os.path import dirname, abspath SECRET_KEY = 'random-secret-key' SESSION_COOKIE_NAME = 'psa_session' -DEBUG = False -from os.path import dirname, abspath +DEBUG = True SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/test.db' % dirname(abspath(__file__)) - DEBUG_TB_INTERCEPT_REDIRECTS = False SESSION_PROTECTION = 'strong' diff --git a/social/apps/flask_app/default/models.py b/social/apps/flask_app/default/models.py index c284d0911..556adc1b9 100644 --- a/social/apps/flask_app/default/models.py +++ b/social/apps/flask_app/default/models.py @@ -1,6 +1,8 @@ """Flask SQLAlchemy ORM models for Social Auth""" from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, backref +from sqlalchemy.schema import UniqueConstraint +from sqlalchemy.ext.declarative import declarative_base from social.utils import setting_name, module_member from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ @@ -10,52 +12,65 @@ BaseSQLAlchemyStorage +PSABase = declarative_base() + + +class _AppSession(PSABase): + __abstract__ = True + + @classmethod + def _set_session(cls, app_session): + cls.app_session = app_session + + @classmethod + def _session(cls): + return cls.app_session + + +class UserSocialAuth(_AppSession, SQLAlchemyUserMixin): + """Social Auth association model""" + # Temporary override of constraints to avoid an error on the still-to-be + # missing column uid. + __table_args__ = () + + @classmethod + def user_model(cls): + return cls.user.property.argument + + @classmethod + def username_max_length(cls): + user_model = cls.user_model() + return user_model.__table__.columns.get('username').type.length + + +class Nonce(_AppSession, SQLAlchemyNonceMixin): + """One use numbers""" + pass + + +class Association(_AppSession, SQLAlchemyAssociationMixin): + """OpenId account association""" + pass + + +class Code(_AppSession, SQLAlchemyCodeMixin): + pass + + class FlaskStorage(BaseSQLAlchemyStorage): - user = None - nonce = None - association = None - code = None + user = UserSocialAuth + nonce = Nonce + association = Association + code = Code -def init_social(app, db): +def init_social(app, session): UID_LENGTH = app.config.get(setting_name('UID_LENGTH'), 255) User = module_member(app.config[setting_name('USER_MODEL')]) - app_session = db.session - - class _AppSession(object): - @classmethod - def _session(cls): - return app_session - - class UserSocialAuth(_AppSession, db.Model, SQLAlchemyUserMixin): - """Social Auth association model""" - uid = Column(String(UID_LENGTH)) - user_id = Column(Integer, ForeignKey(User.id), - nullable=False, index=True) - user = relationship(User, backref=backref('social_auth', - lazy='dynamic')) - - @classmethod - def username_max_length(cls): - return User.__table__.columns.get('username').type.length - - @classmethod - def user_model(cls): - return User - - class Nonce(_AppSession, db.Model, SQLAlchemyNonceMixin): - """One use numbers""" - pass - - class Association(_AppSession, db.Model, SQLAlchemyAssociationMixin): - """OpenId account association""" - pass - - class Code(_AppSession, db.Model, SQLAlchemyCodeMixin): - pass - - # Set the references in the storage class - FlaskStorage.user = UserSocialAuth - FlaskStorage.nonce = Nonce - FlaskStorage.association = Association - FlaskStorage.code = Code + _AppSession._set_session(session) + UserSocialAuth.__table_args__ = (UniqueConstraint('provider', 'uid'),) + UserSocialAuth.uid = Column(String(UID_LENGTH)) + UserSocialAuth.user_id = Column(Integer, ForeignKey(User.id), + nullable=False, index=True) + UserSocialAuth.user = relationship(User, backref=backref('social_auth', + lazy='dynamic')) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index f1934c434..1be8a1a58 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -3,10 +3,11 @@ import six import json +from sqlalchemy import Column, Integer, String from sqlalchemy.exc import IntegrityError from sqlalchemy.types import PickleType, Text -from sqlalchemy import Column, Integer, String from sqlalchemy.schema import UniqueConstraint +from sqlalchemy.ext.declarative import declared_attr from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage From 30fec8813fd48817934c06274d4be241204c7110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 24 Nov 2014 12:48:16 -0200 Subject: [PATCH 419/890] Remove Flask-SQLAlchemy dependency from example app --- examples/flask_example/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/flask_example/requirements.txt b/examples/flask_example/requirements.txt index 97de88fe2..418fc8650 100644 --- a/examples/flask_example/requirements.txt +++ b/examples/flask_example/requirements.txt @@ -1,7 +1,6 @@ Flask -Flask-SQLAlchemy Flask-Login Flask-Script Werkzeug pysqlite -Jinja2 \ No newline at end of file +Jinja2 From d0872f7dc15a4d5dfc7a51ea86a4b8bd8ec8cf71 Mon Sep 17 00:00:00 2001 From: Lukas Klein Date: Mon, 24 Nov 2014 19:52:08 +0100 Subject: [PATCH 420/890] Removed Orkut backend As of September 30, 2014, Orkut [has been shut down](https://support.google.com/orkut/?hl=en), so there's no need for the corresponding backend anymore. --- README.rst | 2 - docs/backends/google.rst | 22 +-------- docs/backends/oauth.rst | 4 +- docs/intro.rst | 2 - examples/django_example/example/settings.py | 1 - social/backends/orkut.py | 50 --------------------- 6 files changed, 4 insertions(+), 77 deletions(-) delete mode 100644 social/backends/orkut.py diff --git a/README.rst b/README.rst index 2e03140fc..e4a04882e 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,6 @@ or current ones extended): * OpenId_ * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth * OpenSuse_ OpenId http://en.opensuse.org/openSUSE:Connect - * Orkut_ OAuth1 * PixelPin_ OAuth2 * Pocket_ OAuth2 * Podio_ OAuth2 @@ -255,7 +254,6 @@ check `django-social-auth LICENSE`_ for details: .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _Odnoklassniki: http://www.odnoklassniki.ru -.. _Orkut: http://www.orkut.com .. _Pocket: http://getpocket.com .. _Podio: https://podio.com .. _Shopify: http://shopify.com diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 32b1ed444..aa1c970b7 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -133,24 +133,7 @@ whitelists can be applied too, check the whitelists_ settings for details. Orkut ----- -Orkut offers per application keys named ``Consumer Key`` and ``Consumer Secret``. -To enable Orkut these two keys are needed. - -Check `Google support`_ and `Orkut API`_ for details on getting keys. - -- fill ``Consumer Key`` and ``Consumer Secret`` values:: - - SOCIAL_AUTH_ORKUT_KEY = '' - SOCIAL_AUTH_ORKUT_SECRET = '' - -- add any needed extra data to:: - - SOCIAL_AUTH_ORKUT_EXTRA_DATA = [...] - -- configure extra scopes in:: - - SOCIAL_AUTH_ORKUT_SCOPE = [...] - +As of September 30, 2014, Orkut has been `shut down`_. User identification ------------------- @@ -214,14 +197,13 @@ supporting them you can default to the old values by defining this setting:: SOCIAL_AUTH_GOOGLE_PLUS_USE_DEPRECATED_API = True .. _Google support: http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105 -.. _Orkut API: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating .. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html .. _Google OAuth: http://code.google.com/apis/accounts/docs/OAuth.html .. _Google OAuth2: http://code.google.com/apis/accounts/docs/OAuth2.html .. _OAuth2 Registering: http://code.google.com/apis/accounts/docs/OAuth2.html#Registering .. _OAuth2 draft: http://tools.ietf.org/html/draft-ietf-oauth-v2-10 .. _OAuth reference: http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth -.. _Orkut OAuth: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating +.. _shut down: https://support.google.com/orkut/?csw=1#Authenticating .. _Google Data Protocol Directory: http://code.google.com/apis/gdata/docs/directory.html .. _whitelists: ../configuration/settings.html#whitelists .. _Google+ Sign In: https://developers.google.com/+/web/signin/ diff --git a/docs/backends/oauth.rst b/docs/backends/oauth.rst index 64e593bea..44b1137f4 100644 --- a/docs/backends/oauth.rst +++ b/docs/backends/oauth.rst @@ -2,8 +2,8 @@ OAuth ===== OAuth_ communication demands a set of keys exchange to validate the client -authenticity prior to user approbation. Twitter, Facebook and Orkut -facilitates these keys by application registration, Google works the same, +authenticity prior to user approbation. Twitter, and Facebook facilitates +these keys by application registration, Google works the same, but provides the option for unregistered applications. Check next sections for details. diff --git a/docs/intro.rst b/docs/intro.rst index 623e58043..8e1282615 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -62,7 +62,6 @@ or extend current one): * `Mozilla Persona`_ * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ - * Orkut_ OAuth1 * Podio_ OAuth2 * Rdio_ OAuth1 and OAuth2 * Readability_ OAuth1 @@ -140,7 +139,6 @@ section. .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _Odnoklassniki: http://www.odnoklassniki.ru -.. _Orkut: http://www.orkut.com .. _Podio: https://podio.com .. _Shopify: http://shopify.com .. _Skyrock: https://skyrock.com diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index edd1b66f6..492cb19a7 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -162,7 +162,6 @@ 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', 'social.backends.open_id.OpenIdAuth', 'social.backends.openstreetmap.OpenStreetMapOAuth', - 'social.backends.orkut.OrkutOAuth', 'social.backends.persona.PersonaAuth', 'social.backends.podio.PodioOAuth2', 'social.backends.rdio.RdioOAuth1', diff --git a/social/backends/orkut.py b/social/backends/orkut.py deleted file mode 100644 index aa5053c14..000000000 --- a/social/backends/orkut.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Orkut OAuth backend, docs at: - http://psa.matiasaguirre.net/docs/backends/google.html#orkut -""" -from social.backends.google import GoogleOAuth - - -class OrkutOAuth(GoogleOAuth): - """Orkut OAuth authentication backend""" - name = 'orkut' - DEFAULT_SCOPE = ['http://orkut.gmodules.com/social/'] - - def get_user_details(self, response): - """Return user details from Orkut account""" - try: - emails = response['emails'][0]['value'] - except (KeyError, IndexError): - emails = '' - - fullname, first_name, last_name = self.get_user_names( - fullname=response['displayName'], - first_name=response['name']['givenName'], - last_name=response['name']['familyName'] - ) - return {'username': response['displayName'], - 'email': emails, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from Orkut service""" - fields = ','.join(set(['name', 'displayName', 'emails'] + - self.setting('EXTRA_DATA', []))) - scope = self.DEFAULT_SCOPE + self.setting('SCOPE', []) - params = {'method': 'people.get', - 'id': 'myself', - 'userId': '@me', - 'groupId': '@self', - 'fields': fields, - 'scope': self.SCOPE_SEPARATOR.join(scope)} - url = 'http://www.orkut.com/social/rpc' - request = self.oauth_request(access_token, url, params) - return self.get_json(request.to_url())['data'] - - def oauth_request(self, token, url, params=None): - params = params or {} - scope = self.DEFAULT_SCOPE + self.setting('SCOPE', []) - params['scope'] = self.SCOPE_SEPARATOR.join(scope) - return super(OrkutOAuth, self).oauth_request(token, url, params) From 22ea009c917342fe24ca5581558f27f823c398e1 Mon Sep 17 00:00:00 2001 From: Sasha Golubev Date: Wed, 26 Nov 2014 04:06:55 +0300 Subject: [PATCH 421/890] Added backend for professionali.ru --- social/backends/professionali.py | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 social/backends/professionali.py diff --git a/social/backends/professionali.py b/social/backends/professionali.py new file mode 100644 index 000000000..7f0ca2700 --- /dev/null +++ b/social/backends/professionali.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +professionaly OAuth 2.0 support. + +This contribution adds support for professionaly.ru OAuth 2.0. +Username is retrieved from the identity returned by server. +""" +from time import time + +from social.backends.oauth import BaseOAuth2 +from social.utils import parse_qs + + +class ProfessionaliOAuth2(BaseOAuth2): + name = 'professionali' + ID_KEY = 'user_id' + EXTRA_DATA = [('avatar_big', 'avatar_big'), + ('link', 'link')] + AUTHORIZATION_URL = 'https://api.professionali.ru/oauth/authorize.html' + ACCESS_TOKEN_URL = 'https://api.professionali.ru/oauth/getToken.json' + ACCESS_TOKEN_METHOD = 'POST' + + def get_user_details(self, response): + first_name, last_name = map(response.get, ('firstname', 'lastname')) + email = (self.setting('FAKE_EMAIL') + and '%s@professionali.ru' % time() + or '') + return {'username': '%s_%s' % (last_name, first_name), + 'first_name': first_name, + 'last_name': last_name, + 'email': email} + + def user_data(self, access_token, response, *args, **kwargs): + url = 'https://api.professionali.ru/v6/users/get.json' + default_fields = list(set(['firstname', 'lastname', + 'avatar_big', 'link'] + + self.setting('EXTRA_DATA', []))) + fields = ','.join(default_fields) + params = {'fields': fields, + 'access_token': access_token, + 'ids[]': response['user_id']} + try: + return self.get_json(url, params)[0] + except (TypeError, KeyError, IOError, ValueError, IndexError): + return None + + def get_json(self, url, *args, **kwargs): + return self.request(url, verify=False, *args, **kwargs).json() + + def get_querystring(self, url, *args, **kwargs): + return parse_qs(self.request(url, verify=False, *args, **kwargs).text) From a2e22cb4eade5b0d551dd80eafe8f889c0dc56f0 Mon Sep 17 00:00:00 2001 From: Anna Warzecha Date: Wed, 26 Nov 2014 10:16:11 +0100 Subject: [PATCH 422/890] User ID is required to use any further requests --- social/backends/khanacademy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py index 48655865f..891292bce 100644 --- a/social/backends/khanacademy.py +++ b/social/backends/khanacademy.py @@ -105,6 +105,8 @@ class KhanAcademyOAuth1(BrowserBasedOAuth1): REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' USER_DATA_URL = 'https://www.khanacademy.org/api/v1/user' + EXTRA_DATA = [('user_id', 'user_id')] + def get_user_details(self, response): """Return user details from Khan Academy account""" return { From 38f86ddfd15026f06afaa64a946bf67c43719ca6 Mon Sep 17 00:00:00 2001 From: James Potter Date: Thu, 27 Nov 2014 12:20:42 +0000 Subject: [PATCH 423/890] Update django.rst Instructions are broken when using South / Django 1.6 as South will try to use the default 'migrations' directory and fail. This tells South to use the alternate directory. --- docs/configuration/django.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index 8e409795d..b093434a5 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -40,7 +40,14 @@ Database ./manage.py makemigrations +If you're still using South, you'll need override SOUTH_MIGRATION_MODULES_:: + SOUTH_MIGRATION_MODULES = { + 'default': 'social.apps.django_app.default.south_migrations' + } + +Note that Django's app labels take the last part of the import, so +in this case ``social.apps.django_app.default`` becomes ``default`` here. Sync database to create needed models:: @@ -204,3 +211,4 @@ The fields listed **must** be user models fields. .. _Django built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/django_app .. _AUTHENTICATION_BACKENDS: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#authentication-backends .. _django@dc43fbc: https://github.com/django/django/commit/dc43fbc2f21c12e34e309d0e8a121020391aa03a +.. _SOUTH_MIGRATION_MODULES: http://south.readthedocs.org/en/latest/settings.html#south-migration-modules From fc949b7dd578daa0902106e6dfbc51f4f6ee7072 Mon Sep 17 00:00:00 2001 From: Frankie Robertson Date: Fri, 19 Dec 2014 16:39:22 +0200 Subject: [PATCH 424/890] Fix #460: Call force_text on _URL settings to support reverse_lazy with default session serializer --- social/strategies/django_strategy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 6cc4365f4..c738dd559 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -6,6 +6,7 @@ from django.shortcuts import redirect from django.template import TemplateDoesNotExist, RequestContext, loader from django.utils.datastructures import MergeDict +from django.utils.encoding import force_text from django.utils.translation import get_language from social.strategies.base import BaseStrategy, BaseTemplateStrategy @@ -30,7 +31,10 @@ def __init__(self, storage, request=None, tpl=None): super(DjangoStrategy, self).__init__(storage, tpl) def get_setting(self, name): - return getattr(settings, name) + value = getattr(settings, name) + if name.endswith('_URL'): + value = force_text(value) + return value def request_data(self, merge=True): if not self.request: From 6a5741e85ae0fc1041f1833ceb76f638f3a4f149 Mon Sep 17 00:00:00 2001 From: Alex Muller Date: Wed, 24 Dec 2014 15:56:50 +0000 Subject: [PATCH 425/890] Update GitHub documentation - Capitalise GitHub consistently - Change App Id and Secret to reflect GitHub's naming - Use gender-neutral language when talking about people --- docs/backends/github.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/backends/github.rst b/docs/backends/github.rst index 57a86bd90..382c7426c 100644 --- a/docs/backends/github.rst +++ b/docs/backends/github.rst @@ -1,13 +1,13 @@ GitHub ====== -Github works similar to Facebook (OAuth). +GitHub works similar to Facebook (OAuth). - Register a new application at `GitHub Developers`_, set the callback URL to ``http://example.com/complete/github/`` replacing ``example.com`` with your domain. -- Fill ``App Id`` and ``App Secret`` values in the settings:: +- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings:: SOCIAL_AUTH_GITHUB_KEY = '' SOCIAL_AUTH_GITHUB_SECRET = '' @@ -17,37 +17,37 @@ Github works similar to Facebook (OAuth). SOCIAL_AUTH_GITHUB_SCOPE = [...] -Github for Organizations +GitHub for Organizations ------------------------ When defining authentication for organizations, use the -``GithubOrganizationOAuth2`` backend instead. The settings are the same than +``GithubOrganizationOAuth2`` backend instead. The settings are the same as the non-organization backend, but the names must be:: - SOCIAL_AUTH_GITHUB_ORG_* + SOCIAL_AUTH_GITHUB_ORG_* Be sure to define the organization name using the setting:: SOCIAL_AUTH_GITHUB_ORG_NAME = '' This name will be used to check that the user really belongs to the given -organization and discard it in case he's not part of it. +organization and discard it if they're not part of it. -Github for Teams +GitHub for Teams ---------------- -Similar to ``Github for Organizations``, there's a Github for Teams backend, -use the backend ``GithubTeamOAuth2``. The settings are the same than +Similar to ``GitHub for Organizations``, there's a GitHub for Teams backend, +use the backend ``GithubTeamOAuth2``. The settings are the same as the basic backend, but the names must be:: SOCIAL_AUTH_GITHUB_TEAM_* -Be sure to define the ``Team Id`` using the setting:: +Be sure to define the ``Team ID`` using the setting:: SOCIAL_AUTH_GITHUB_TEAM_ID = '' This ``id`` will be used to check that the user really belongs to the given -team and discard it in case he's not part of it. +team and discard it if they're not part of it. .. _GitHub Developers: https://github.com/settings/applications/new From 7e1c9b72ef9b8d663b19482cf45900d06116e1ad Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Sun, 28 Dec 2014 21:27:55 -0700 Subject: [PATCH 426/890] Slack backend --- social/backends/slack.py | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 social/backends/slack.py diff --git a/social/backends/slack.py b/social/backends/slack.py new file mode 100644 index 000000000..3f6eb0d42 --- /dev/null +++ b/social/backends/slack.py @@ -0,0 +1,60 @@ +""" +Slack OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/slack.html + https://api.slack.com/docs/oauth +""" +from social.backends.oauth import BaseOAuth2 +import re + + +class SlackOAuth2(BaseOAuth2): + """Slack OAuth authentication backend""" + name = 'slack' + AUTHORIZATION_URL = 'https://slack.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://slack.com/api/oauth.access' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' + EXTRA_DATA = [ + ('id', 'id'), + ('name', 'name'), + ('real_name', 'real_name') + ] + REDIRECT_STATE = True + + def get_user_details(self, response): + """Return user details from Slack account""" + + # Build the username with the team $username@$team_url + # Necessary to get unique names for all of slack + match = re.search("//([^.]+)\.slack\.com", response["team_url"]) + username = "%s@%s" % (response.get("name"), match.group(1)) + + return {'username': username, + 'email': response["profile"].get('email', ''), + 'fullname': response["profile"].get("real_name"), + 'first_name': response["profile"].get("first_name"), + 'last_name': response["profile"].get("last_name") + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + # Has to be two calls, because the users.info requires a username, + # And we want the team information + # https://api.slack.com/methods/auth.test + auth_test = self.get_json('https://slack.com/api/auth.test', params={ + 'token': access_token + }) + + # https://api.slack.com/methods/users.info + data = self.get_json('https://slack.com/api/users.info', params={ + 'token': access_token, + 'user': auth_test.get("user_id") + }) + + # Inject the team data + out = data["user"].copy() + out["team_id"] = auth_test.get("team_id") + out["team"] = auth_test.get("team") + out["team_url"] = auth_test.get("url") + + return out From 6c1743c121e7cde45fa214219127fd148a732ea2 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Sun, 28 Dec 2014 21:30:04 -0700 Subject: [PATCH 427/890] Documentation for slack backend --- docs/backends/slack.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/backends/slack.rst diff --git a/docs/backends/slack.rst b/docs/backends/slack.rst new file mode 100644 index 000000000..5c4be2780 --- /dev/null +++ b/docs/backends/slack.rst @@ -0,0 +1,19 @@ +Slack +====== + +Slack + +- Register a new application at `https://api.slack.com/applications`_, set the callback URL to + ``http://example.com/complete/slack/`` replacing ``example.com`` with your + domain. + +- Fill ``Client ID`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_SLACK_KEY = '' + SOCIAL_AUTH_SLACK_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_SLACK_SCOPE = [...] + + See auth scopes at https://api.slack.com/docs/oauth From 0a31ebb656cf7385f421018631b335ec9acf1ec7 Mon Sep 17 00:00:00 2001 From: Alex Muller Date: Mon, 29 Dec 2014 11:46:15 +0000 Subject: [PATCH 428/890] Correct Django SESSION_COOKIE_AGE setting I think the duplication of SOCIAL_AUTH_SESSION_EXPIRATION in this section is a typo. --- docs/configuration/settings.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 846381dca..c0bf74380 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -262,7 +262,7 @@ Miscellaneous settings ``SOCIAL_AUTH_SESSION_EXPIRATION = False`` By default, user session expiration time will be set by your web framework (in Django, for example, it is set with - SOCIAL_AUTH_SESSION_EXPIRATION). Some providers return the time that the + `SESSION_COOKIE_AGE`_). Some providers return the time that the access token will live, which is stored in ``UserSocialAuth.extra_data`` under the key ``expires``. Changing this setting to True will override your web framework's session length setting and set user session lengths to @@ -303,3 +303,4 @@ using POST. .. _OAuth: http://oauth.net/ .. _passwordless authentication mechanism: https://medium.com/@ninjudd/passwords-are-obsolete-9ed56d483eb .. _psa-passwordless: https://github.com/omab/psa-passwordless +.. _SESSION_COOKIE_AGE: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-SESSION_COOKIE_AGE From f19f571938c14d55a2f06f97ca00c0e8aa4606cf Mon Sep 17 00:00:00 2001 From: travoltino Date: Fri, 2 Jan 2015 01:01:00 +0200 Subject: [PATCH 429/890] Update base.py Added the ability to configure the certificate validation. Change through settings.py SOCIAL_AUTH_NAME_VERIFY_SSL = False to disable certificate validation. --- social/backends/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/backends/base.py b/social/backends/base.py index 607ec0315..7a800a10a 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -209,6 +209,8 @@ def uses_redirect(self): def request(self, url, method='GET', *args, **kwargs): kwargs.setdefault('headers', {}) + if not self.setting('VERIFY_SSL') == None: + kwargs.setdefault('verify', self.setting('VERIFY_SSL')) kwargs.setdefault('timeout', self.setting('REQUESTS_TIMEOUT') or self.setting('URLOPEN_TIMEOUT')) From 5dad0e398f694fb8fde12f0a706b60767e0dbdd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 2 Jan 2015 04:40:16 -0200 Subject: [PATCH 430/890] Change expression --- social/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/base.py b/social/backends/base.py index 7a800a10a..4b5c666e3 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -209,7 +209,7 @@ def uses_redirect(self): def request(self, url, method='GET', *args, **kwargs): kwargs.setdefault('headers', {}) - if not self.setting('VERIFY_SSL') == None: + if self.setting('VERIFY_SSL') is not None: kwargs.setdefault('verify', self.setting('VERIFY_SSL')) kwargs.setdefault('timeout', self.setting('REQUESTS_TIMEOUT') or self.setting('URLOPEN_TIMEOUT')) From a84f6a1da55c1ca8f0f0ace7fc6ae046b194fda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 2 Jan 2015 04:45:26 -0200 Subject: [PATCH 431/890] Link docs, apply PEP8, change quotes and code style --- docs/backends/index.rst | 1 + docs/backends/slack.rst | 10 +++++----- social/backends/slack.py | 36 +++++++++++++++++------------------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index ae9afe54f..262d68fd6 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -100,6 +100,7 @@ Social backends salesforce shopify skyrock + slack soundcloud spotify suse diff --git a/docs/backends/slack.rst b/docs/backends/slack.rst index 5c4be2780..831a9deeb 100644 --- a/docs/backends/slack.rst +++ b/docs/backends/slack.rst @@ -1,11 +1,11 @@ Slack -====== +===== Slack -- Register a new application at `https://api.slack.com/applications`_, set the callback URL to - ``http://example.com/complete/slack/`` replacing ``example.com`` with your - domain. +- Register a new application at `https://api.slack.com/applications`_, set the + callback URL to ``http://example.com/complete/slack/`` replacing + ``example.com`` with your domain. - Fill ``Client ID`` and ``Client Secret`` values in the settings:: @@ -16,4 +16,4 @@ Slack SOCIAL_AUTH_SLACK_SCOPE = [...] - See auth scopes at https://api.slack.com/docs/oauth + See auth scopes at https://api.slack.com/docs/oauth diff --git a/social/backends/slack.py b/social/backends/slack.py index 3f6eb0d42..ec0446c8d 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -3,9 +3,10 @@ http://psa.matiasaguirre.net/docs/backends/slack.html https://api.slack.com/docs/oauth """ -from social.backends.oauth import BaseOAuth2 import re +from social.backends.oauth import BaseOAuth2 + class SlackOAuth2(BaseOAuth2): """Slack OAuth authentication backend""" @@ -14,27 +15,26 @@ class SlackOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://slack.com/api/oauth.access' ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' + REDIRECT_STATE = True EXTRA_DATA = [ ('id', 'id'), ('name', 'name'), ('real_name', 'real_name') ] - REDIRECT_STATE = True def get_user_details(self, response): """Return user details from Slack account""" - # Build the username with the team $username@$team_url # Necessary to get unique names for all of slack - match = re.search("//([^.]+)\.slack\.com", response["team_url"]) - username = "%s@%s" % (response.get("name"), match.group(1)) - - return {'username': username, - 'email': response["profile"].get('email', ''), - 'fullname': response["profile"].get("real_name"), - 'first_name': response["profile"].get("first_name"), - 'last_name': response["profile"].get("last_name") - } + match = re.search(r'//([^.]+)\.slack\.com', response['team_url']) + username = '{0}@{1}'.format(response.get('name'), match.group(1)) + return { + 'username': username, + 'email': response['profile'].get('email', ''), + 'fullname': response['profile'].get('real_name'), + 'first_name': response['profile'].get('first_name'), + 'last_name': response['profile'].get('last_name') + } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" @@ -48,13 +48,11 @@ def user_data(self, access_token, *args, **kwargs): # https://api.slack.com/methods/users.info data = self.get_json('https://slack.com/api/users.info', params={ 'token': access_token, - 'user': auth_test.get("user_id") + 'user': auth_test.get('user_id') }) - # Inject the team data - out = data["user"].copy() - out["team_id"] = auth_test.get("team_id") - out["team"] = auth_test.get("team") - out["team_url"] = auth_test.get("url") - + out = data['user'].copy() + out['team_id'] = auth_test.get('team_id') + out['team'] = auth_test.get('team') + out['team_url'] = auth_test.get('url') return out From 6d97ff65ffca7aefae85f76e1734fa7c75b66f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 2 Jan 2015 05:00:19 -0200 Subject: [PATCH 432/890] PEP8 --- social/backends/professionali.py | 40 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/social/backends/professionali.py b/social/backends/professionali.py index 7f0ca2700..5e79ba96b 100644 --- a/social/backends/professionali.py +++ b/social/backends/professionali.py @@ -1,44 +1,48 @@ # -*- coding: utf-8 -*- """ -professionaly OAuth 2.0 support. +Professionaly OAuth 2.0 support. This contribution adds support for professionaly.ru OAuth 2.0. Username is retrieved from the identity returned by server. """ from time import time -from social.backends.oauth import BaseOAuth2 from social.utils import parse_qs +from social.backends.oauth import BaseOAuth2 class ProfessionaliOAuth2(BaseOAuth2): name = 'professionali' ID_KEY = 'user_id' - EXTRA_DATA = [('avatar_big', 'avatar_big'), - ('link', 'link')] AUTHORIZATION_URL = 'https://api.professionali.ru/oauth/authorize.html' ACCESS_TOKEN_URL = 'https://api.professionali.ru/oauth/getToken.json' ACCESS_TOKEN_METHOD = 'POST' + EXTRA_DATA = [ + ('avatar_big', 'avatar_big'), + ('link', 'link') + ] def get_user_details(self, response): first_name, last_name = map(response.get, ('firstname', 'lastname')) - email = (self.setting('FAKE_EMAIL') - and '%s@professionali.ru' % time() - or '') - return {'username': '%s_%s' % (last_name, first_name), - 'first_name': first_name, - 'last_name': last_name, - 'email': email} + email = '' + if self.setting('FAKE_EMAIL'): + email = '{0}@professionali.ru'.format(time()) + return { + 'username': '{0}_{1}'.format(last_name, first_name), + 'first_name': first_name, + 'last_name': last_name, + 'email': email + } def user_data(self, access_token, response, *args, **kwargs): url = 'https://api.professionali.ru/v6/users/get.json' - default_fields = list(set(['firstname', 'lastname', - 'avatar_big', 'link'] - + self.setting('EXTRA_DATA', []))) - fields = ','.join(default_fields) - params = {'fields': fields, - 'access_token': access_token, - 'ids[]': response['user_id']} + fields = list(set(['firstname', 'lastname', 'avatar_big', 'link'] + + self.setting('EXTRA_DATA', []))) + params = { + 'fields': ','.join(fields), + 'access_token': access_token, + 'ids[]': response['user_id'] + } try: return self.get_json(url, params)[0] except (TypeError, KeyError, IOError, ValueError, IndexError): From d87e12ad3a205cebb30181a9b4c4234d538d8f72 Mon Sep 17 00:00:00 2001 From: Jun Wang Date: Fri, 2 Jan 2015 09:04:53 -0500 Subject: [PATCH 433/890] Fix YahooOAuth get primary email sorting order --- social/backends/yahoo.py | 2 +- social/tests/backends/test_yahoo.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 746b9b37f..206949e04 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -34,7 +34,7 @@ def get_user_details(self, response): ) emails = [email for email in response.get('emails', []) if email.get('handle')] - emails.sort(key=lambda e: e.get('primary', False)) + emails.sort(key=lambda e: e.get('primary', False), reverse=True) return {'username': response.get('nickname'), 'email': emails[0]['handle'] if emails else '', 'fullname': fullname, diff --git a/social/tests/backends/test_yahoo.py b/social/tests/backends/test_yahoo.py index d3c87ae77..ec4d21c99 100644 --- a/social/tests/backends/test_yahoo.py +++ b/social/tests/backends/test_yahoo.py @@ -1,4 +1,5 @@ import json +import requests from httpretty import HTTPretty from social.p3 import urlencode @@ -41,7 +42,19 @@ class YahooOAuth1Test(OAuth1Test): 'isConnected': False, 'profileUrl': 'http://profile.yahoo.com/a-guid', 'guid': 'a-guid', - 'nickname': 'foobar' + 'nickname': 'foobar', + 'emails': [{ + 'handle': 'foobar@yahoo.com', + 'id': 1, + 'primary': True, + 'type': 'HOME', + }, + { + 'handle': 'foobar@email.com', + 'id': 2, + 'type': 'HOME', + }], + } }) @@ -56,3 +69,14 @@ def test_login(self): def test_partial_pipeline(self): self.do_partial_pipeline() + + def test_get_user_details(self): + HTTPretty.register_uri( + HTTPretty.GET, + self.user_data_url, + status=200, + body=self.user_data_body + ) + response = requests.get(self.user_data_url) + user_details=self.backend.get_user_details(response.json()['profile']) + self.assertEqual(user_details['email'], 'foobar@yahoo.com') From 55d010d851461fd8502185dd3a80211f06f4dcb1 Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Mon, 5 Jan 2015 16:59:09 -0600 Subject: [PATCH 434/890] Fixed extra_data field in django 1.7 initial migration --- social/apps/django_app/default/migrations/0001_initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 309f5db2e..f08827802 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -78,7 +78,7 @@ class Migration(migrations.Migration): ('provider', models.CharField(max_length=32)), ('uid', models.CharField(max_length=255)), ('extra_data', social.apps.django_app.default.fields.JSONField( - default=b'{}')), + default='{}')), ('user', models.ForeignKey( related_name='social_auth', to=user_model)), ], From 30ee628c4701adb95f0c93cfc16b570a079ced6f Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Wed, 7 Jan 2015 08:23:45 -0800 Subject: [PATCH 435/890] when scope is reduced, the response from slack is different, handle both --- social/backends/slack.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/social/backends/slack.py b/social/backends/slack.py index 3f6eb0d42..5ea49c86b 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -51,10 +51,17 @@ def user_data(self, access_token, *args, **kwargs): 'user': auth_test.get("user_id") }) - # Inject the team data - out = data["user"].copy() - out["team_id"] = auth_test.get("team_id") - out["team"] = auth_test.get("team") - out["team_url"] = auth_test.get("url") + # Capture the user data, if available based on the scope + if data.get("user"): + out = data["user"].copy() + # inject the team data + out["team_id"] = auth_test.get("team_id") + out["team"] = auth_test.get("team") + out["team_url"] = auth_test.get("url") + else: + out = data.copy() + # make the data consistent with the above + out["id"] = out["user_id"] + del out["user_id"] return out From e044e10d112f11a8ffc72658c695b0a56f787f18 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Wed, 7 Jan 2015 10:26:33 -0800 Subject: [PATCH 436/890] update in a way that will be more future proof --- social/backends/slack.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/social/backends/slack.py b/social/backends/slack.py index 5ea49c86b..d2973cad1 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -60,8 +60,6 @@ def user_data(self, access_token, *args, **kwargs): out["team_url"] = auth_test.get("url") else: out = data.copy() - # make the data consistent with the above - out["id"] = out["user_id"] - del out["user_id"] + out.update(auth_test) return out From dbab84921539222ca7d90d4fac49c089decd6694 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Wed, 7 Jan 2015 15:26:26 -0800 Subject: [PATCH 437/890] properly handle data, so that it is more future proof, again. This time fix issue with team_url --- social/backends/slack.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/social/backends/slack.py b/social/backends/slack.py index d2973cad1..bd02e156d 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -26,7 +26,7 @@ def get_user_details(self, response): # Build the username with the team $username@$team_url # Necessary to get unique names for all of slack - match = re.search("//([^.]+)\.slack\.com", response["team_url"]) + match = re.search("//([^.]+)\.slack\.com", response["url"]) username = "%s@%s" % (response.get("name"), match.group(1)) return {'username': username, @@ -51,15 +51,15 @@ def user_data(self, access_token, *args, **kwargs): 'user': auth_test.get("user_id") }) - # Capture the user data, if available based on the scope if data.get("user"): - out = data["user"].copy() - # inject the team data - out["team_id"] = auth_test.get("team_id") - out["team"] = auth_test.get("team") - out["team_url"] = auth_test.get("url") + # Capture the user data, if available based on the scope + out = data["user"] else: - out = data.copy() - out.update(auth_test) + # Otherwise, grab whatever is available + out = data + + # inject the auth/team data. Most notably so we can get the slack url, + # for creating unique usernames + out.update(auth_test) return out From d46aa12a81478a1b681b2d6e0264c88524949548 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Wed, 7 Jan 2015 19:18:33 -0800 Subject: [PATCH 438/890] cleanly handle both a scope of 'identity' only and also fill in more data if we have 'read' access --- social/backends/slack.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/social/backends/slack.py b/social/backends/slack.py index bd02e156d..bbf51cd32 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -27,14 +27,17 @@ def get_user_details(self, response): # Build the username with the team $username@$team_url # Necessary to get unique names for all of slack match = re.search("//([^.]+)\.slack\.com", response["url"]) - username = "%s@%s" % (response.get("name"), match.group(1)) + username = "%s@%s" % (response.get("user"), match.group(1)) - return {'username': username, - 'email': response["profile"].get('email', ''), + out = {'username': username} + if response.get("profile"): + out.update({ + 'email': response["profile"].get("email"), 'fullname': response["profile"].get("real_name"), 'first_name': response["profile"].get("first_name"), 'last_name': response["profile"].get("last_name") - } + }) + return out def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" @@ -44,22 +47,21 @@ def user_data(self, access_token, *args, **kwargs): auth_test = self.get_json('https://slack.com/api/auth.test', params={ 'token': access_token }) + out = auth_test + del out["ok"] # https://api.slack.com/methods/users.info - data = self.get_json('https://slack.com/api/users.info', params={ + user_info = self.get_json('https://slack.com/api/users.info', params={ 'token': access_token, 'user': auth_test.get("user_id") }) - if data.get("user"): + if user_info.get("user"): # Capture the user data, if available based on the scope - out = data["user"] - else: - # Otherwise, grab whatever is available - out = data + out.update(user_info["user"]) - # inject the auth/team data. Most notably so we can get the slack url, - # for creating unique usernames - out.update(auth_test) + # Clean up user_id vs id + out["id"] = out["user_id"] + del out["user_id"] return out From b5533e70e262a16cbd046111fe08efab85fabb0e Mon Sep 17 00:00:00 2001 From: Chris Barna Date: Sat, 10 Jan 2015 16:50:26 -0500 Subject: [PATCH 439/890] Store Spotify's refresh_token. --- social/backends/spotify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/backends/spotify.py b/social/backends/spotify.py index 0c2d3c834..3074a4501 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -16,6 +16,9 @@ class SpotifyOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False + EXTRA_DATA = [ + ('refresh_token', 'refresh_token'), + ] def auth_headers(self): return { From 2c0c8e0b598ac6f8e5e97d3e65beb4e7b73a0475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 19 Jan 2015 12:25:59 -0200 Subject: [PATCH 440/890] Patch tornado arguments/cookies getting. Refs #445. Refs #346 --- social/strategies/tornado_strategy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index b30b2d1cb..0f2a525ec 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -27,7 +27,8 @@ def get_setting(self, name): return self.request_handler.settings[name] def request_data(self, merge=True): - return self.request.arguments.copy() + # Multiple valued arguments not supported yet + return {key: val[0] for key, val in self.request.arguments.iteritems()} def request_host(self): return self.request.host @@ -39,7 +40,7 @@ def html(self, content): self.request_handler.write(content) def session_get(self, name, default=None): - return self.request_handler.get_secure_cookie(name, value=default) + return self.request_handler.get_secure_cookie(name) or default def session_set(self, name, value): self.request_handler.set_secure_cookie(name, str(value)) From 501d7353747d66d45fd2ea50ce404a3d5ba71505 Mon Sep 17 00:00:00 2001 From: ayush Date: Tue, 20 Jan 2015 12:54:56 -0800 Subject: [PATCH 441/890] Added nonce unique constraint --- social/apps/django_app/default/migrations/0001_initial.py | 4 ++++ social/apps/django_app/default/models.py | 1 + 2 files changed, 5 insertions(+) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 309f5db2e..392fddfed 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -95,4 +95,8 @@ class Migration(migrations.Migration): name='code', unique_together=set([('email', 'code')]), ), + migrations.AlterUniqueTogether( + name='nonce', + unique_together=set([('server_url', 'timestamp', 'salt')]), + ), ] diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index 600e9f197..e30e003bb 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -71,6 +71,7 @@ class Nonce(models.Model, DjangoNonceMixin): salt = models.CharField(max_length=65) class Meta: + unique_together = ('server_url', 'timestamp', 'salt') db_table = 'social_auth_nonce' From 9173ace70150e3e04ad07fc15ddd165dbe4d03ea Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Fri, 23 Jan 2015 15:33:22 +0100 Subject: [PATCH 442/890] Added backend for Coursera --- social/backends/coursera.py | 39 ++++++++++++++++++++++++++ social/tests/backends/test_coursera.py | 37 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 social/backends/coursera.py create mode 100644 social/tests/backends/test_coursera.py diff --git a/social/backends/coursera.py b/social/backends/coursera.py new file mode 100644 index 000000000..9507edbbd --- /dev/null +++ b/social/backends/coursera.py @@ -0,0 +1,39 @@ +""" +Coursera OAuth2 backend, docs at: + https://tech.coursera.org/app-platform/oauth2/ +""" +from social.backends.oauth import BaseOAuth2 + + +class CourseraOAuth2(BaseOAuth2): + """Coursera OAuth2 authentication backend""" + name = 'coursera' + ID_KEY = 'username' + AUTHORIZATION_URL = 'https://accounts.coursera.org/oauth2/v1/auth' + ACCESS_TOKEN_URL = 'https://accounts.coursera.org/oauth2/v1/token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + SCOPE_SEPARATOR = ',' + DEFAULT_SCOPE = ['view_profile'] + + def _get_username_from_response(self, response): + elements = response.get('elements', []) + for element in elements: + if 'id' in element: + return element.get('id') + + return None + + def get_user_details(self, response): + """Return user details from Coursera account""" + return {'username': self._get_username_from_response(response)} + + def user_data(self, access_token, *args, **kwargs): + """Load user data from the service""" + return self.get_json( + 'https://api.coursera.org/api/externalBasicProfiles.v1?q=me', + headers=self.get_auth_header(access_token) + ) + + def get_auth_header(self, access_token): + return {'Authorization': 'Bearer {0}'.format(access_token)} diff --git a/social/tests/backends/test_coursera.py b/social/tests/backends/test_coursera.py new file mode 100644 index 000000000..ad6836e5f --- /dev/null +++ b/social/tests/backends/test_coursera.py @@ -0,0 +1,37 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class CourseraOAuth2Test(OAuth2Test): + backend_path = 'social.backends.coursera.CourseraOAuth2' + user_data_url = 'https://api.coursera.org/api/externalBasicProfiles.v1?q=me' + expected_username = '560e7ed2076e0d589e88bd74b6aad4b7' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'Bearer', + 'expires_in': 1795 + }) + request_token_body = json.dumps({ + 'code': 'foobar-code', + 'client_id': 'foobar-client-id', + 'client_secret': 'foobar-client-secret', + 'redirect_uri': 'http://localhost:8000/accounts/coursera/', + 'grant_type': 'authorization_code' + }) + user_data_body = json.dumps({ + 'token_type': 'Bearer', + 'paging': None, + 'elements': [{ + 'id': '560e7ed2076e0d589e88bd74b6aad4b7' + }], + 'access_token': 'foobar', + 'expires_in': 1800, + 'linked': None + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From f9e9a95e37d0e2006da284ea0921b5c178866872 Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Fri, 23 Jan 2015 15:33:40 +0100 Subject: [PATCH 443/890] Added Coursera backend to django_example --- examples/django_example/example/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 492cb19a7..e1e3072e4 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -130,6 +130,7 @@ 'social.backends.box.BoxOAuth2', 'social.backends.clef.ClefOAuth2', 'social.backends.coinbase.CoinbaseOAuth2', + 'social.backends.coursera.CourseraOAuth2', 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.douban.DoubanOAuth2', From 50a4bc70ee13ca5ae48f89e7a1e6784855322a9b Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Tue, 27 Jan 2015 10:15:17 +0100 Subject: [PATCH 444/890] Docs for coursera backend --- docs/backends/coursera.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/backends/coursera.rst diff --git a/docs/backends/coursera.rst b/docs/backends/coursera.rst new file mode 100644 index 000000000..5e23a7a3c --- /dev/null +++ b/docs/backends/coursera.rst @@ -0,0 +1,26 @@ +Coursera +============ + +Coursera uses a variant of OAuth2 authentication. The details of the API +can be found at `OAuth2-based APIs - Coursera Technology`_. + +Take the following steps in order to use the backend: + +1. Create an account at `Coursera`_. + +2. Open `Developer Console`_, create an organisation and application. + +3. Set **Client ID** as a ``SOCIAL_AUTH_COURSERA_KEY`` and +**Secret Key** as a ``SOCIAL_AUTH_COURSERA_SECRET`` in your local settings. + +4. Add the backend to ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.coursera.CourseraOAuth2', + ... + ) + +.. _OAuth2-based APIs - Coursera Technology: https://tech.coursera.org/app-platform/oauth2/ +.. _Coursera: https://accounts.coursera.org/console +.. _Developer Console: https://accounts.coursera.org/console From 35f442894e4f4af338c6e05963cfa79c3334116b Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Tue, 27 Jan 2015 10:21:57 +0100 Subject: [PATCH 445/890] Added coursera backend to README --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e4a04882e..72382bf7d 100644 --- a/README.rst +++ b/README.rst @@ -61,6 +61,7 @@ or current ones extended): * Bitbucket_ OAuth1 * Box_ OAuth2 * Clef_ OAuth2 + * Coursera_ OAuth2 * Dailymotion_ OAuth2 * Disqus_ OAuth2 * Douban_ OAuth1 and OAuth2 @@ -189,14 +190,14 @@ Or:: $ cd python-social-auth $ sudo python setup.py install - + Upgrading --------- Django with South ~~~~~~~~~~~~~~~~~ -Upgrading from 0.1 to 0.2 is likely to cause problems trying to apply a migration when the tables +Upgrading from 0.1 to 0.2 is likely to cause problems trying to apply a migration when the tables already exist. In this case a fake migration needs to be applied:: $ python manage.py migrate --fake default @@ -232,6 +233,7 @@ check `django-social-auth LICENSE`_ for details: .. _Bitbucket: https://bitbucket.org .. _Box: https://www.box.com .. _Clef: https://getclef.com/ +.. _Coursera: https://www.coursera.org/ .. _Dailymotion: https://dailymotion.com .. _Disqus: https://disqus.com .. _Douban: http://www.douban.com From 0dc2d42451a108ec596f9c9a7f65033ec8034e71 Mon Sep 17 00:00:00 2001 From: rivf Date: Fri, 30 Jan 2015 04:44:06 +0300 Subject: [PATCH 446/890] Fixed jawbone authentification --- social/backends/jawbone.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index e0fe9d440..0df42c7da 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -52,6 +52,15 @@ def process_error(self, data): )) return super(JawboneOAuth2, self).process_error(data) + def auth_complete_params(self, state=None): + client_id, client_secret = self.get_key_and_secret() + return { + 'grant_type': 'authorization_code', # request auth code + 'code': self.data.get('code', ''), # server response code + 'client_id': client_id, + 'client_secret': client_secret, + } + def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) From e9ac5dc3f266d1108deb5d84f89ca3014a3e7d6e Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Mon, 2 Feb 2015 14:32:41 +1100 Subject: [PATCH 447/890] Add support for Launchpad OpenId Add support for Launchpad OpenId --- README.rst | 2 ++ docs/backends/launchpad.rst | 11 +++++++++++ social/backends/launchpad.py | 11 +++++++++++ 3 files changed, 24 insertions(+) create mode 100644 docs/backends/launchpad.rst create mode 100644 social/backends/launchpad.py diff --git a/README.rst b/README.rst index e4a04882e..bd097d045 100644 --- a/README.rst +++ b/README.rst @@ -79,6 +79,7 @@ or current ones extended): * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication * Kakao_ OAuth2 https://developer.kakao.com * `Khan Academy`_ OAuth1 + * Launchpad_ OpenId * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId @@ -245,6 +246,7 @@ check `django-social-auth LICENSE`_ for details: .. _Github: https://github.com .. _Google: http://google.com .. _Instagram: https://instagram.com +.. _LaunchPad: https://help.launchpad.net/YourAccount/OpenID .. _Linkedin: https://www.linkedin.com .. _Live: https://live.com .. _Livejournal: http://livejournal.com diff --git a/docs/backends/launchpad.rst b/docs/backends/launchpad.rst new file mode 100644 index 000000000..7f12f35fe --- /dev/null +++ b/docs/backends/launchpad.rst @@ -0,0 +1,11 @@ +Launchpad +========= + +`Ubuntu Launchpad `_ OpenId doesn't require +major settings beside being defined on ``AUTHENTICATION_BACKENDS```:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.launchpad.LaunchpadOpenId', + ... + ) diff --git a/social/backends/launchpad.py b/social/backends/launchpad.py new file mode 100644 index 000000000..2f4a51390 --- /dev/null +++ b/social/backends/launchpad.py @@ -0,0 +1,11 @@ +""" +Launchpad OpenId backend +""" + +from social.backends.open_id import OpenIdAuth + + +class LaunchpadOpenId(OpenIdAuth): + name = 'launchpad' + URL = 'https://login.launchpad.net' + USERNAME_KEY = 'nickname' From efa68bc0a059c8da2a2fa5e5e66ae5908a9533af Mon Sep 17 00:00:00 2001 From: Chris DeBlois Date: Mon, 2 Feb 2015 23:52:52 +0000 Subject: [PATCH 448/890] updated mendeley oauth2 to use new api resource and also updated to grab new profile_id, name and bio --- social/backends/mendeley.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index 48464e76b..fde57a575 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -12,13 +12,13 @@ class MendeleyMixin(object): ('bio', 'bio')] def get_user_id(self, details, response): - return response['main']['profile_id'] + return response['id'] def get_user_details(self, response): """Return user details from Mendeley account""" - profile_id = response['main']['profile_id'] - name = response['main']['name'] - bio = response['main']['bio'] + profile_id = response['id'] + name = response['display_name'] + bio = response['link'] return {'profile_id': profile_id, 'name': name, 'bio': bio} @@ -26,7 +26,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Return user data provided""" values = self.get_user_data(access_token) - values.update(values['main']) + values.update(values) return values def get_user_data(self, access_token): @@ -62,6 +62,6 @@ class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): def get_user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( - 'https://api-oauth2.mendeley.com/oapi/profiles/info/me/', + 'https://api.mendeley.com/profiles/me/', headers={'Authorization': 'Bearer {0}'.format(access_token)} ) From 5721df64b4ffe23b68c54a7fe945caa9fa545aa7 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 3 Feb 2015 11:45:11 +1100 Subject: [PATCH 449/890] Ensure email is not None If a user chooses not to share their email via the OpenId login page, we end up passing through None as the 'email' value, which ends up raising a "column 'email' cannot be null" error on some databases. Blank string is fine. --- social/backends/open_id.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 0fd28f5db..11c2bfc64 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -96,6 +96,7 @@ def get_user_details(self, response): fullname = values.get('fullname') or '' first_name = values.get('first_name') or '' last_name = values.get('last_name') or '' + email = values.get('email') or '' if not fullname and first_name and last_name: fullname = first_name + ' ' + last_name @@ -109,7 +110,8 @@ def get_user_details(self, response): values.update({'fullname': fullname, 'first_name': first_name, 'last_name': last_name, 'username': values.get(username_key) or - (first_name.title() + last_name.title())}) + (first_name.title() + last_name.title()), + 'email': email}) return values def extra_data(self, user, uid, response, details): From 91127e2364418318ea954bca13dd9338069231bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 3 Feb 2015 01:30:38 -0200 Subject: [PATCH 450/890] Enable debug pipeline in example app --- examples/django_example/example/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 492cb19a7..7ea0aaa9d 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -229,7 +229,7 @@ 'social.pipeline.debug.debug', 'social.pipeline.social_auth.load_extra_data', 'social.pipeline.user.user_details', - #'social.pipeline.debug.debug' + 'social.pipeline.debug.debug' ) TEST_RUNNER = 'django.test.runner.DiscoverRunner' From 6348c63b6ed569f2b4314eef5158e48bbaa3b2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 3 Feb 2015 02:02:13 -0200 Subject: [PATCH 451/890] Define methods to customize urls in OAuth2 backends --- examples/django_example/example/settings.py | 1 + social/backends/oauth.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 7ea0aaa9d..2ba917015 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -159,6 +159,7 @@ 'social.backends.mendeley.MendeleyOAuth2', 'social.backends.mineid.MineIDOAuth2', 'social.backends.mixcloud.MixcloudOAuth2', + 'social.backends.nationbuilder.NationBuilderOAuth2', 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', 'social.backends.open_id.OpenIdAuth', 'social.backends.openstreetmap.OpenStreetMapOAuth', diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 169643975..0e33a0470 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -324,7 +324,7 @@ def auth_url(self): # redirect_uri matching is strictly enforced, so match the # providers value exactly. params = unquote(params) - return self.AUTHORIZATION_URL + '?' + params + return '{0}?{1}'.format(self.authorization_url(), params) def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() @@ -358,7 +358,7 @@ def auth_complete(self, *args, **kwargs): self.process_error(self.data) try: response = self.request_access_token( - self.ACCESS_TOKEN_URL, + self.access_token_url(), data=self.auth_complete_params(state), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD @@ -396,7 +396,7 @@ def process_refresh_token_response(self, response, *args, **kwargs): def refresh_token(self, token, *args, **kwargs): params = self.refresh_token_params(token, *args, **kwargs) - url = self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL + url = self.refresh_token_url() method = self.REFRESH_TOKEN_METHOD key = 'params' if method == 'GET' else 'data' request_args = {'headers': self.auth_headers(), @@ -404,3 +404,12 @@ def refresh_token(self, token, *args, **kwargs): key: params} request = self.request(url, **request_args) return self.process_refresh_token_response(request, *args, **kwargs) + + def authorization_url(self): + return self.AUTHORIZATION_URL + + def access_token_url(self): + return self.ACCESS_TOKEN_URL + + def refresh_token_url(self): + return self.REFRESH_TOKEN_URL or self.access_token_url() From bb295abdc8abda7ad0b29288c87b7d0eed9e2610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 3 Feb 2015 02:08:53 -0200 Subject: [PATCH 452/890] NationBuilder backend --- docs/backends/index.rst | 1 + docs/backends/nationbuilder.rst | 30 ++++++++++++++++++++ social/backends/nationbuilder.py | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 docs/backends/nationbuilder.rst create mode 100644 social/backends/nationbuilder.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 262d68fd6..6af6473f5 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -86,6 +86,7 @@ Social backends mineid mixcloud moves + nationbuilder odnoklassnikiru openstreetmap persona diff --git a/docs/backends/nationbuilder.rst b/docs/backends/nationbuilder.rst new file mode 100644 index 000000000..a27c3eca2 --- /dev/null +++ b/docs/backends/nationbuilder.rst @@ -0,0 +1,30 @@ +NationBuilder +============= + +`NationBuilder supports OAuth2`_ as their authentication mechanism. Follow these +steps in order to use it: + +- Register a new application at your `Nation Admin panel`_ (define the `Callback + URL` to ``http://example.com/complete/nationbuilder/`` where ``example.com`` + is your domain). + +- Fill the ``Client ID`` and ``Client Secret`` values from the newly created + application:: + + SOCIAL_AUTH_NATIONBUILDER_KEY = '' + SOCIAL_AUTH_NATIONBUILDER_SECRET = '' + +- Also define your NationBuilder slug:: + + SOCIAL_AUTH_NATIONBUILDER_SLUG = 'your-nationbuilder-slug' + +- Enable the backend in ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.nationbuilder.NationBuilderOAuth2' + ... + ) + +.. _Nation Admin panel: https://psa.nationbuilder.com/admin/apps +.. _NationBuilder supports OAuth2: http://nationbuilder.com/api_quickstart diff --git a/social/backends/nationbuilder.py b/social/backends/nationbuilder.py new file mode 100644 index 000000000..ae16c7fa8 --- /dev/null +++ b/social/backends/nationbuilder.py @@ -0,0 +1,48 @@ +""" +NationBuilder OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/nationbuilder.html +""" +from social.backends.oauth import BaseOAuth2 + + +class NationBuilderOAuth2(BaseOAuth2): + """NationBuilder OAuth2 authentication backend""" + name = 'nationbuilder' + AUTHORIZATION_URL = 'https://{slug}.nationbuilder.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://{slug}.nationbuilder.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + SCOPE_SEPARATOR = ',' + EXTRA_DATA = [ + ('id', 'id'), + ('expires', 'expires') + ] + + def authorization_url(self): + return self.AUTHORIZATION_URL.format(slug=self.slug) + + def access_token_url(self): + return self.ACCESS_TOKEN_URL.format(slug=self.slug) + + @property + def slug(self): + return self.setting('SLUG') + + def get_user_details(self, response): + """Return user details from Github account""" + email = response.get('email') or '' + username = email.split('@')[0] if email else '' + return {'username': username, + 'email': email, + 'fullname': response.get('full_name') or '', + 'first_name': response.get('first_name') or '', + 'last_name': response.get('last_name') or ''} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + url = 'https://{slug}.nationbuilder.com/api/v1/people/me'.format( + slug=self.slug + ) + return self.get_json(url, params={ + 'access_token': access_token + })['person'] From 71c1028811cd82102518a1aaf0ca87ec6d7265ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 3 Feb 2015 02:36:20 -0200 Subject: [PATCH 453/890] Move common code to base class --- social/backends/oauth.py | 42 ++++++++++++++++------------------ social/tests/backends/oauth.py | 5 ++-- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 0e33a0470..717092dc0 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -20,14 +20,20 @@ class OAuthAuth(BaseAuth): name (all uppercase) plus _EXTRA_DATA. access_token is always stored. + + URLs settings: + AUTHORIZATION_URL Authorization service url + ACCESS_TOKEN_URL Access token URL """ - SCOPE_PARAMETER_NAME = 'scope' - DEFAULT_SCOPE = None - SCOPE_SEPARATOR = ' ' - ID_KEY = 'id' + AUTHORIZATION_URL = '' + ACCESS_TOKEN_URL = '' ACCESS_TOKEN_METHOD = 'GET' REVOKE_TOKEN_URL = None REVOKE_TOKEN_METHOD = 'POST' + ID_KEY = 'id' + SCOPE_PARAMETER_NAME = 'scope' + DEFAULT_SCOPE = None + SCOPE_SEPARATOR = ' ' REDIRECT_STATE = False STATE_PARAMETER = False @@ -109,6 +115,12 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service. Implement in subclass""" return {} + def authorization_url(self): + return self.AUTHORIZATION_URL + + def access_token_url(self): + return self.ACCESS_TOKEN_URL + def revoke_token_url(self, token, uid): return self.REVOKE_TOKEN_URL @@ -137,16 +149,14 @@ class BaseOAuth1(OAuthAuth): """Consumer based mechanism OAuth authentication, fill the needed parameters to communicate properly with authentication service. - AUTHORIZATION_URL Authorization service url + URLs settings: REQUEST_TOKEN_URL Request token URL - ACCESS_TOKEN_URL Access token URL + """ - AUTHORIZATION_URL = '' REQUEST_TOKEN_URL = '' REQUEST_TOKEN_METHOD = 'GET' OAUTH_TOKEN_PARAMETER_NAME = 'oauth_token' REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' - ACCESS_TOKEN_URL = '' UNATHORIZED_TOKEN_SUFIX = 'unauthorized_token_name' def auth_url(self): @@ -252,7 +262,7 @@ def oauth_authorization_request(self, token): ) state = self.get_or_create_state() params[self.REDIRECT_URI_PARAMETER_NAME] = self.get_redirect_uri(state) - return self.AUTHORIZATION_URL + '?' + urlencode(params) + return '{0}?{1}'.format(self.authorization_url(), urlencode(params)) def oauth_auth(self, token=None, oauth_verifier=None, signature_type=SIGNATURE_TYPE_AUTH_HEADER): @@ -278,7 +288,7 @@ def oauth_request(self, token, url, params=None, method='GET'): def access_token(self, token): """Return request for access token value""" - return self.get_querystring(self.ACCESS_TOKEN_URL, + return self.get_querystring(self.access_token_url(), auth=self.oauth_auth(token), method=self.ACCESS_TOKEN_METHOD) @@ -288,13 +298,7 @@ class BaseOAuth2(OAuthAuth): OAuth2 draft details at: http://tools.ietf.org/html/draft-ietf-oauth-v2-10 - - Attributes: - AUTHORIZATION_URL Authorization service url - ACCESS_TOKEN_URL Token URL """ - AUTHORIZATION_URL = None - ACCESS_TOKEN_URL = None REFRESH_TOKEN_URL = None REFRESH_TOKEN_METHOD = 'POST' RESPONSE_TYPE = 'code' @@ -405,11 +409,5 @@ def refresh_token(self, token, *args, **kwargs): request = self.request(url, **request_args) return self.process_refresh_token_response(request, *args, **kwargs) - def authorization_url(self): - return self.AUTHORIZATION_URL - - def access_token_url(self): - return self.ACCESS_TOKEN_URL - def refresh_token_url(self): return self.REFRESH_TOKEN_URL or self.access_token_url() diff --git a/social/tests/backends/oauth.py b/social/tests/backends/oauth.py index 0169bd238..9b9c84960 100644 --- a/social/tests/backends/oauth.py +++ b/social/tests/backends/oauth.py @@ -59,7 +59,7 @@ def auth_handlers(self, start_url): status=200, body='foobar') HTTPretty.register_uri(self._method(self.backend.ACCESS_TOKEN_METHOD), - uri=self.backend.ACCESS_TOKEN_URL, + uri=self.backend.access_token_url(), status=self.access_token_status, body=self.access_token_body or '', content_type='text/json') @@ -107,8 +107,7 @@ def refresh_token_arguments(self): def do_refresh_token(self): self.do_login() HTTPretty.register_uri(self._method(self.backend.REFRESH_TOKEN_METHOD), - self.backend.REFRESH_TOKEN_URL or - self.backend.ACCESS_TOKEN_URL, + self.backend.refresh_token_url(), status=200, body=self.refresh_token_body) user = list(User.cache.values())[0] From 609f598c726bdd7513df565184f281a7bd789abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 3 Feb 2015 02:36:31 -0200 Subject: [PATCH 454/890] Add test for nationbuilder backend --- social/tests/backends/test_nationbuilder.py | 232 ++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 social/tests/backends/test_nationbuilder.py diff --git a/social/tests/backends/test_nationbuilder.py b/social/tests/backends/test_nationbuilder.py new file mode 100644 index 000000000..70d357fd6 --- /dev/null +++ b/social/tests/backends/test_nationbuilder.py @@ -0,0 +1,232 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class NationBuilderOAuth2Test(OAuth2Test): + backend_path = 'social.backends.nationbuilder.NationBuilderOAuth2' + user_data_url = 'https://foobar.nationbuilder.com/api/v1/people/me' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer', + 'created_at': 1422937981, + 'expires_in': 2592000 + }) + user_data_body = json.dumps({ + 'person': { + 'twitter_followers_count': None, + 'last_name': 'Bar', + 'rule_violations_count': 0, + 'linkedin_id': None, + 'recruiter_id': None, + 'membership_expires_at': None, + 'donations_raised_count': 0, + 'last_contacted_at': None, + 'prefix': None, + 'profile_content_html': None, + 'email4': None, + 'email2': None, + 'availability': None, + 'occupation': None, + 'user_submitted_address': None, + 'could_vote_status': None, + 'state_upper_district': None, + 'salesforce_id': None, + 'van_id': None, + 'phone_time': None, + 'profile_content': None, + 'auto_import_id': None, + 'parent_id': None, + 'email4_is_bad': False, + 'twitter_updated_at': None, + 'email3_is_bad': False, + 'bio': None, + 'party_member': None, + 'unsubscribed_at': None, + 'fax_number': None, + 'last_contacted_by': None, + 'active_customer_expires_at': None, + 'federal_donotcall': False, + 'warnings_count': 0, + 'first_supporter_at': '2015-02-02T19:30:28-08:00', + 'previous_party': None, + 'donations_raised_amount_this_cycle_in_cents': 0, + 'call_status_name': None, + 'marital_status': None, + 'facebook_updated_at': None, + 'donations_count': 0, + 'note_updated_at': None, + 'closed_invoices_count': None, + 'profile_headline': None, + 'fire_district': None, + 'mobile_normalized': None, + 'import_id': None, + 'last_call_id': None, + 'donations_raised_amount_in_cents': 0, + 'facebook_address': None, + 'is_profile_private': False, + 'last_rule_violation_at': None, + 'sex': None, + 'full_name': 'Foo Bar', + 'last_donated_at': None, + 'donations_pledged_amount_in_cents': 0, + 'primary_email_id': 1, + 'media_market_name': None, + 'capital_amount_in_cents': 500, + 'datatrust_id': None, + 'precinct_code': None, + 'email3': None, + 'religion': None, + 'first_prospect_at': None, + 'judicial_district': None, + 'donations_count_this_cycle': 0, + 'work_address': None, + 'is_twitter_follower': False, + 'email1': 'foobar@gmail.com', + 'email': 'foobar@gmail.com', + 'contact_status_name': None, + 'mobile_opt_in': True, + 'twitter_description': None, + 'parent': None, + 'tags': [], + 'first_volunteer_at': None, + 'inferred_support_level': None, + 'banned_at': None, + 'first_invoice_at': None, + 'donations_raised_count_this_cycle': 0, + 'is_donor': False, + 'twitter_location': None, + 'email1_is_bad': False, + 'legal_name': None, + 'language': None, + 'registered_at': None, + 'call_status_id': None, + 'last_invoice_at': None, + 'school_sub_district': None, + 'village_district': None, + 'twitter_name': None, + 'membership_started_at': None, + 'subnations': [], + 'meetup_address': None, + 'author_id': None, + 'registered_address': None, + 'external_id': None, + 'twitter_login': None, + 'inferred_party': None, + 'spent_capital_amount_in_cents': 0, + 'suffix': None, + 'mailing_address': None, + 'is_leaderboardable': True, + 'twitter_website': None, + 'nbec_guid': None, + 'city_district': None, + 'church': None, + 'is_profile_searchable': True, + 'employer': None, + 'is_fundraiser': False, + 'email_opt_in': True, + 'recruits_count': 0, + 'email2_is_bad': False, + 'county_district': None, + 'recruiter': None, + 'twitter_friends_count': None, + 'facebook_username': None, + 'active_customer_started_at': None, + 'pf_strat_id': None, + 'locale': None, + 'twitter_address': None, + 'is_supporter': True, + 'do_not_call': False, + 'profile_image_url_ssl': 'https://d3n8a8pro7vhmx.cloudfront.net' + '/assets/icons/buddy.png', + 'invoices_amount_in_cents': None, + 'username': None, + 'donations_amount_in_cents': 0, + 'is_volunteer': False, + 'civicrm_id': None, + 'supranational_district': None, + 'precinct_name': None, + 'invoice_payments_amount_in_cents': None, + 'work_phone_number': None, + 'phone': '213.394.4623', + 'received_capital_amount_in_cents': 500, + 'primary_address': None, + 'is_possible_duplicate': False, + 'invoice_payments_referred_amount_in_cents': None, + 'donations_amount_this_cycle_in_cents': 0, + 'priority_level': None, + 'first_fundraised_at': None, + 'phone_normalized': '2133944623', + 'rnc_regid': None, + 'twitter_id': None, + 'birthdate': None, + 'mobile': None, + 'federal_district': None, + 'donations_to_raise_amount_in_cents': 0, + 'support_probability_score': None, + 'invoices_count': None, + 'nbec_precinct_code': None, + 'website': None, + 'closed_invoices_amount_in_cents': None, + 'home_address': None, + 'school_district': None, + 'support_level': None, + 'demo': None, + 'children_count': 0, + 'updated_at': '2015-02-02T19:30:28-08:00', + 'membership_level_name': None, + 'billing_address': None, + 'is_ignore_donation_limits': False, + 'signup_type': 0, + 'precinct_id': None, + 'rnc_id': None, + 'id': 2, + 'ethnicity': None, + 'is_survey_question_private': False, + 'middle_name': None, + 'author': None, + 'last_fundraised_at': None, + 'state_file_id': None, + 'note': None, + 'submitted_address': None, + 'support_level_changed_at': None, + 'party': None, + 'contact_status_id': None, + 'outstanding_invoices_amount_in_cents': None, + 'page_slug': None, + 'outstanding_invoices_count': None, + 'first_recruited_at': None, + 'county_file_id': None, + 'first_name': 'Foo', + 'facebook_profile_url': None, + 'city_sub_district': None, + 'has_facebook': False, + 'is_deceased': False, + 'labour_region': None, + 'state_lower_district': None, + 'dw_id': None, + 'created_at': '2015-02-02T19:30:28-08:00', + 'is_prospect': False, + 'priority_level_changed_at': None, + 'is_mobile_bad': False, + 'overdue_invoices_count': None, + 'ngp_id': None, + 'do_not_contact': False, + 'first_donated_at': None, + 'turnout_probability_score': None + }, + 'precinct': None + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_NATIONBUILDER_SLUG': 'foobar' + }) + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_NATIONBUILDER_SLUG': 'foobar' + }) + self.do_partial_pipeline() From 81fa59452527e776573e7239222347cf7a524c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mu=C3=B1oz=20C=C3=A1rdenas?= Date: Fri, 6 Feb 2015 18:09:25 +0100 Subject: [PATCH 455/890] Update Google documentation Add the deprecation warning for OAuth 1.0 according: https://developers.google.com/accounts/docs/OAuth Update Google+ Sign-In documentation completing more information and applying the latest changes. Source: https://developers.google.com/+/web/signin/ --- docs/backends/google.rst | 97 ++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index aa1c970b7..1ef2dce52 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -6,6 +6,11 @@ This section describes how to setup the different services provided by Google. Google OAuth ------------ +.. attention:: **Google OAuth deprecation** + Important: OAuth 1.0 was officially deprecated on April 20, 2012, and will be + shut down on April 20, 2015. We encourage you to migrate to any of the other + protocols. + Google provides ``Consumer Key`` and ``Consumer Secret`` keys to registered applications, but also allows unregistered application to use their authorization system with, but beware that this method will display a security banner to the @@ -59,69 +64,72 @@ done by their Javascript which thens calls a defined handler to complete the auth process. * To enable the backend create an application using the `Google console`_ and - fill the key settings:: + following the steps from the `official guide`_. + +* Fill in the key settings looking inside the Google console the subsection + ``Credentials`` inside ``API & auth``:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.google.GooglePlusAuth', + ) SOCIAL_AUTH_GOOGLE_PLUS_KEY = '...' SOCIAL_AUTH_GOOGLE_PLUS_SECRET = '...' -* Add their button snippet to your template:: + ``SOCIAL_AUTH_GOOGLE_PLUS_KEY`` corresponds to the variable ``CLIENT ID``. + ``SOCIAL_AUTH_GOOGLE_PLUS_SECRET`` corresponds to the variable + ``CLIENT SECRET``. + +* Create a new Django view and in its template add the Google+ Sign-In button::
          -
          - ``signInCallback`` is the name of your Javascript callback function. - -* The scope can be generated doing:: - - from social.backends.google import GooglePlusAuth - plus_scope = ' '.join(GooglePlusAuth.DEFAULT_SCOPE) + + {% csrf_token %} + + + - Or get the value from settings if it was overridden. ``plus_id`` is the value - from ``SOCIAL_AUTH_GOOGLE_PLUS_KEY``. + ``plus_id`` is the value from ``SOCIAL_AUTH_GOOGLE_PLUS_KEY``. + ``signInCallback`` is the name of your Javascript callback function. -* Add the Javascript snippet:: +* Add the Javascript snippet in the same template as above:: - + -* Define your Javascript callback function:: +* And define your Javascript callback function:: - In the example above the values needed to complete the auth process are - posted using a form like this but this is just a simple example:: - -
          {% csrf_token %} - - -
          - Google OpenId ------------- @@ -208,5 +216,6 @@ supporting them you can default to the old values by defining this setting:: .. _whitelists: ../configuration/settings.html#whitelists .. _Google+ Sign In: https://developers.google.com/+/web/signin/ .. _Google console: https://code.google.com/apis/console +.. _official guide: https://developers.google.com/+/web/signin/#step_1_create_a_client_id_and_client_secret .. _Sept 1, 2014: https://developers.google.com/+/api/auth-migration#timetable .. _e3525187: https://github.com/omab/python-social-auth/commit/e35251878a88954cecf8e575eca27c63164b9f67 From bdf69d67d109acfda1016d4a2a63a1cc0a3aba84 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Fri, 6 Feb 2015 02:08:30 -0500 Subject: [PATCH 456/890] Updated PyJWT Dependency - Using PyJWT 0.4.1 (or newer) - Relying on PyJWT to verify ID token audience and issuer --- requirements-python3.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- social/backends/open_id.py | 15 +++------------ social/tests/backends/open_id.py | 4 ++-- social/tests/requirements-python3.txt | 2 +- social/tests/requirements.txt | 2 +- 7 files changed, 10 insertions(+), 19 deletions(-) diff --git a/requirements-python3.txt b/requirements-python3.txt index bb7f4f532..22c68d3da 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -3,4 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.0,<0.3.2 six>=1.2.0 -PyJWT==0.2.1 +PyJWT==0.4.1 diff --git a/requirements.txt b/requirements.txt index 960b9da5a..b0b0b9564 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.0 six>=1.2.0 -PyJWT==0.2.1 +PyJWT==0.4.1 diff --git a/setup.py b/setup.py index c94788686..c6b468406 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT>=0.2.1'] +requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT==0.4.1'] if PY3: requires += ['python3-openid>=3.0.1', 'requests-oauthlib>=0.3.0,<0.3.2'] diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 0fd28f5db..2c0cc0d98 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -1,7 +1,7 @@ import datetime from calendar import timegm -from jwt import DecodeError, ExpiredSignature, decode as jwt_decode +from jwt import InvalidTokenError, decode as jwt_decode from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE from openid.consumer.discover import DiscoveryFailure @@ -327,24 +327,15 @@ def validate_and_return_id_token(self, id_token): try: # Decode the JWT and raise an error if the secret is invalid or # the response has expired. - id_token = jwt_decode(id_token, decryption_key) - except (DecodeError, ExpiredSignature) as de: + id_token = jwt_decode(id_token, decryption_key, audience=client_id, issuer=self.ID_TOKEN_ISSUER) + except InvalidTokenError as de: raise AuthTokenError(self, de) - # Verify the issuer of the id_token is correct - if id_token['iss'] != self.ID_TOKEN_ISSUER: - raise AuthTokenError(self, 'Incorrect id_token: iss') - # Verify the token was issued in the last 10 minutes utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) if id_token['iat'] < (utc_timestamp - 600): raise AuthTokenError(self, 'Incorrect id_token: iat') - # Verify this client is the correct recipient of the id_token - aud = id_token.get('aud') - if aud != client_id: - raise AuthTokenError(self, 'Incorrect id_token: aud') - # Validate the nonce to ensure the request was not modified nonce = id_token.get('nonce') if not nonce: diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index c15a91d39..e0d6e01af 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -216,11 +216,11 @@ def test_expired_signature(self): expiration_datetime=expiration_datetime) def test_invalid_issuer(self): - self.authtoken_raised('Token error: Incorrect id_token: iss', + self.authtoken_raised('Token error: Invalid issuer', issuer='someone-else') def test_invalid_audience(self): - self.authtoken_raised('Token error: Incorrect id_token: aud', + self.authtoken_raised('Token error: Invalid audience', client_key='someone-else') def test_invalid_issue_time(self): diff --git a/social/tests/requirements-python3.txt b/social/tests/requirements-python3.txt index 5cc5bfc2d..ea2d9893e 100644 --- a/social/tests/requirements-python3.txt +++ b/social/tests/requirements-python3.txt @@ -3,5 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -PyJWT==0.2.1 +PyJWT==0.4.1 unittest2py3k==0.5.1 diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index e82544764..c8a69d1d5 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,5 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -PyJWT==0.2.1 +PyJWT==0.4.1 unittest2==0.5.1 From e6456416106f3a51555e21b4eb93d2f34e240184 Mon Sep 17 00:00:00 2001 From: Alejandro Baronetti Date: Sat, 7 Feb 2015 12:33:34 +0000 Subject: [PATCH 457/890] Fix: REQUEST has been deprecated in Django 1.7, so we need to merge dictionaries --- social/strategies/django_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 6cc4365f4..0cc6a04d0 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -36,7 +36,8 @@ def request_data(self, merge=True): if not self.request: return {} if merge: - data = self.request.REQUEST + data = self.request.GET + data.update(self.request.POST) elif self.request.method == 'POST': data = self.request.POST else: From 794a28a96ac414aaa054f7c533dd14f33308066b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 10 Feb 2015 00:05:01 -0200 Subject: [PATCH 458/890] PEP8 --- social/backends/open_id.py | 7 ++++--- social/tests/backends/test_coursera.py | 3 ++- social/tests/backends/test_yahoo.py | 10 +++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 17f732de2..0c7b23ea8 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -329,9 +329,10 @@ def validate_and_return_id_token(self, id_token): try: # Decode the JWT and raise an error if the secret is invalid or # the response has expired. - id_token = jwt_decode(id_token, decryption_key, audience=client_id, issuer=self.ID_TOKEN_ISSUER) - except InvalidTokenError as de: - raise AuthTokenError(self, de) + id_token = jwt_decode(id_token, decryption_key, audience=client_id, + issuer=self.ID_TOKEN_ISSUER) + except InvalidTokenError as err: + raise AuthTokenError(self, err) # Verify the token was issued in the last 10 minutes utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) diff --git a/social/tests/backends/test_coursera.py b/social/tests/backends/test_coursera.py index ad6836e5f..059d7cb5e 100644 --- a/social/tests/backends/test_coursera.py +++ b/social/tests/backends/test_coursera.py @@ -5,7 +5,8 @@ class CourseraOAuth2Test(OAuth2Test): backend_path = 'social.backends.coursera.CourseraOAuth2' - user_data_url = 'https://api.coursera.org/api/externalBasicProfiles.v1?q=me' + user_data_url = \ + 'https://api.coursera.org/api/externalBasicProfiles.v1?q=me' expected_username = '560e7ed2076e0d589e88bd74b6aad4b7' access_token_body = json.dumps({ 'access_token': 'foobar', diff --git a/social/tests/backends/test_yahoo.py b/social/tests/backends/test_yahoo.py index ec4d21c99..41128df0d 100644 --- a/social/tests/backends/test_yahoo.py +++ b/social/tests/backends/test_yahoo.py @@ -48,13 +48,11 @@ class YahooOAuth1Test(OAuth1Test): 'id': 1, 'primary': True, 'type': 'HOME', - }, - { + }, { 'handle': 'foobar@email.com', 'id': 2, 'type': 'HOME', }], - } }) @@ -69,7 +67,7 @@ def test_login(self): def test_partial_pipeline(self): self.do_partial_pipeline() - + def test_get_user_details(self): HTTPretty.register_uri( HTTPretty.GET, @@ -78,5 +76,7 @@ def test_get_user_details(self): body=self.user_data_body ) response = requests.get(self.user_data_url) - user_details=self.backend.get_user_details(response.json()['profile']) + user_details = self.backend.get_user_details( + response.json()['profile'] + ) self.assertEqual(user_details['email'], 'foobar@yahoo.com') From 5c9c9f9c43d209d7b79d17692575dfd5dfa4b15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 10 Feb 2015 00:09:21 -0200 Subject: [PATCH 459/890] Pyflakes --- social/backends/jawbone.py | 1 + social/backends/kakao.py | 2 -- social/backends/mineid.py | 3 --- social/p3.py | 7 ++++++- social/storage/sqlalchemy_orm.py | 1 - social/tests/backends/test_mineid.py | 3 --- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index 0df42c7da..cf6735904 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -2,6 +2,7 @@ Jawbone OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/jawbone.html """ +from requests import HTTPError from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthCanceled, AuthUnknownError diff --git a/social/backends/kakao.py b/social/backends/kakao.py index 014934f20..b20cdf956 100644 --- a/social/backends/kakao.py +++ b/social/backends/kakao.py @@ -18,8 +18,6 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Kakao account""" nickname = response['properties']['nickname'] - thumbnail_image = response['properties']['thumbnail_image'] - profile_image = response['properties']['profile_image'] return { 'username': nickname, 'email': '', diff --git a/social/backends/mineid.py b/social/backends/mineid.py index 7bfe97f1a..69dae18f3 100644 --- a/social/backends/mineid.py +++ b/social/backends/mineid.py @@ -1,6 +1,3 @@ -import json -import urllib - from social.backends.oauth import BaseOAuth2 diff --git a/social/p3.py b/social/p3.py index 8816db694..37fce44e1 100644 --- a/social/p3.py +++ b/social/p3.py @@ -1,6 +1,7 @@ -import six # Python3 support, keep import hacks here +import six + if six.PY3: from urllib.parse import parse_qs, urlparse, urlunparse, quote, \ urlsplit, urlencode, unquote @@ -13,3 +14,7 @@ from urlparse import urlparse, urlunparse, urlsplit from urllib import urlencode, unquote, quote from StringIO import StringIO + + +# Placate pyflakes +parse_qs, urlparse, urlunparse, quote, urlsplit, urlencode, unquote, StringIO diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 1be8a1a58..0947792c0 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -7,7 +7,6 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.types import PickleType, Text from sqlalchemy.schema import UniqueConstraint -from sqlalchemy.ext.declarative import declared_attr from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage diff --git a/social/tests/backends/test_mineid.py b/social/tests/backends/test_mineid.py index 6d55d4d79..406a0bb4a 100644 --- a/social/tests/backends/test_mineid.py +++ b/social/tests/backends/test_mineid.py @@ -1,8 +1,5 @@ import json -from social.p3 import urlencode -from social.exceptions import AuthUnknownError - from social.tests.backends.oauth import OAuth2Test From 4c731dee079a5a6771e9c36ab628d5cb20397efd Mon Sep 17 00:00:00 2001 From: tell-k Date: Wed, 11 Feb 2015 21:15:48 +0900 Subject: [PATCH 460/890] add qiita backend --- docs/backends/qiita.rst | 19 +++++++++ social/backends/qiita.py | 66 +++++++++++++++++++++++++++++ social/tests/backends/test_qiita.py | 25 +++++++++++ 3 files changed, 110 insertions(+) create mode 100644 docs/backends/qiita.rst create mode 100644 social/backends/qiita.py create mode 100644 social/tests/backends/test_qiita.py diff --git a/docs/backends/qiita.rst b/docs/backends/qiita.rst new file mode 100644 index 000000000..b1cd5dd2b --- /dev/null +++ b/docs/backends/qiita.rst @@ -0,0 +1,19 @@ +Qiita +===== + +Qiita + +- Register a new application at `https://qiita.com/settings/applications`_, set the + callback URL to ``http://example.com/complete/qiita/`` replacing + ``example.com`` with your domain. + +- Fill ``Client ID`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_QIITA_KEY = '' + SOCIAL_AUTH_QIITA_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_QIITA_SCOPE = [...] + + See auth scopes at https://qiita.com/api/v2/docs#スコープ diff --git a/social/backends/qiita.py b/social/backends/qiita.py new file mode 100644 index 000000000..05a0e47eb --- /dev/null +++ b/social/backends/qiita.py @@ -0,0 +1,66 @@ +""" +Qiita OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/qiita.html + http://qiita.com/api/v2/docs#get-apiv2oauthauthorize +""" +import json + +from social.backends.oauth import BaseOAuth2 + + +class QiitaOAuth2(BaseOAuth2): + """Qiita OAuth authentication backend""" + name = 'qiita' + + AUTHORIZATION_URL = 'https://qiita.com/api/v2/oauth/authorize' + ACCESS_TOKEN_URL = 'https://qiita.com/api/v2/access_tokens' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ' ' + REDIRECT_STATE = True + EXTRA_DATA = [ + ('description', 'description'), + ('facebook_id', 'facebook_id'), + ('followees_count', 'followers_count'), + ('followers_count', 'followers_count'), + ('github_login_name', 'github_login_name'), + ('id', 'id'), + ('items_count', 'items_count'), + ('linkedin_id', 'linkedin_id'), + ('location', 'location'), + ('name', 'name'), + ('organization', 'organization'), + ('profile_image_url', 'profile_image_url'), + ('twitter_screen_name', 'twitter_screen_name'), + ('website_url', 'website_url'), + ] + + def auth_complete_params(self, state=None): + data = super(QiitaOAuth2, self).auth_complete_params(state) + if "grant_type" in data: + del data["grant_type"] + if "redirect_uri" in data: + del data["redirect_uri"] + return json.dumps(data) + + def auth_headers(self): + return {'Content-Type': 'application/json'} + + def request_access_token(self, *args, **kwargs): + data = super(QiitaOAuth2, self).request_access_token(*args, **kwargs) + data.update({'access_token': data['token']}) + return data + + def get_user_details(self, response): + """Return user details from Qiita account""" + return { + 'username': response['id'], + 'fullname': response['name'], + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://qiita.com/api/v2/authenticated_user', + headers={ + 'Authorization': ' Bearer {}'.format(access_token) + }) diff --git a/social/tests/backends/test_qiita.py b/social/tests/backends/test_qiita.py new file mode 100644 index 000000000..b1871d714 --- /dev/null +++ b/social/tests/backends/test_qiita.py @@ -0,0 +1,25 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class QiitaOAuth2Test(OAuth2Test): + backend_path = 'social.backends.qiita.QiitaOAuth2' + user_data_url = 'https://qiita.com/api/v2/authenticated_user' + expected_username = 'foobar' + + access_token_body = json.dumps({ + 'token': 'foobar', + 'token_type': 'bearer' + }) + + user_data_body = json.dumps({ + 'id': 'foobar', + 'name': 'Foo Bar' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From de62c48d1df0287aa1c5aee69640bc952b09ba14 Mon Sep 17 00:00:00 2001 From: tell-k Date: Wed, 11 Feb 2015 21:27:37 +0900 Subject: [PATCH 461/890] refs #512 fixed bug for py2.6 --- social/backends/qiita.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/qiita.py b/social/backends/qiita.py index 05a0e47eb..8cca56ed8 100644 --- a/social/backends/qiita.py +++ b/social/backends/qiita.py @@ -62,5 +62,5 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://qiita.com/api/v2/authenticated_user', headers={ - 'Authorization': ' Bearer {}'.format(access_token) + 'Authorization': ' Bearer {0}'.format(access_token) }) From b2dacd322e5f446a300783b994ddacf1be3b4f1b Mon Sep 17 00:00:00 2001 From: tell-k Date: Wed, 11 Feb 2015 23:00:34 +0900 Subject: [PATCH 462/890] refs #512 fixed typo --- social/backends/qiita.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/qiita.py b/social/backends/qiita.py index 8cca56ed8..52b947a1a 100644 --- a/social/backends/qiita.py +++ b/social/backends/qiita.py @@ -20,7 +20,7 @@ class QiitaOAuth2(BaseOAuth2): EXTRA_DATA = [ ('description', 'description'), ('facebook_id', 'facebook_id'), - ('followees_count', 'followers_count'), + ('followees_count', 'followees_count'), ('followers_count', 'followers_count'), ('github_login_name', 'github_login_name'), ('id', 'id'), From 94ad25e06a82641130ef18eb37a682bb1285baa0 Mon Sep 17 00:00:00 2001 From: Eugene Agafonov Date: Thu, 12 Feb 2015 21:42:21 +0300 Subject: [PATCH 463/890] [facebook-oauth2] Verifying Graph API Calls with appsecret_proof https://developers.facebook.com/docs/graph-api/securing-requests Graph API calls from a server can be better secured by adding a parameter called appsecret_proof. The app secret proof is a sha256 hash of your access token, using the app secret as the key. $appsecret_proof= hash_hmac('sha256', $access_token, $app_secret) the result as an appsecret_proof parameter must be added to each call you make from server. Securing with appsecret_proof is optional (but enabled by default for new Facebook apps) so add appsecret_proof param is contoled by SOCIAL_AUTH_FACEBOOK_APPSECRET_PROOF = True|False Default is True --- social/backends/facebook.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index f318169db..eb7a494b6 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -13,6 +13,11 @@ from social.exceptions import AuthException, AuthCanceled, AuthUnknownError, \ AuthMissingParameter +import hmac +import hashlib + +def hmac_sha256(key, msg): + return hmac.new(key, msg, digestmod=hashlib.sha256).hexdigest() class FacebookOAuth2(BaseOAuth2): """Facebook OAuth2 authentication backend""" @@ -46,6 +51,11 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" params = self.setting('PROFILE_EXTRA_PARAMS', {}) params['access_token'] = access_token + + if self.setting('APPSECRET_PROOF', True): + _, secret = self.get_key_and_secret() + params['appsecret_proof'] = hmac_sha256(secret, access_token); + return self.get_json(self.USER_DATA_URL, params=params) def process_error(self, data): From 9c24e1b792f6434779b249e52bc500f6422f81dc Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Fri, 13 Feb 2015 21:23:05 -0800 Subject: [PATCH 464/890] Include username in Reddit extra_data --- social/backends/reddit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/reddit.py b/social/backends/reddit.py index e40598e66..62ac42cab 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -20,6 +20,7 @@ class RedditOAuth2(BaseOAuth2): SEND_USER_AGENT = True EXTRA_DATA = [ ('id', 'id'), + ('name', 'username'), ('link_karma', 'link_karma'), ('comment_karma', 'comment_karma'), ('refresh_token', 'refresh_token'), From 0884f3a1bce42a1a385689b98f42604fe6db0517 Mon Sep 17 00:00:00 2001 From: Alejandro Baronetti Date: Sat, 14 Feb 2015 11:14:11 +0000 Subject: [PATCH 465/890] Fixed issue: GET dictionary is immutable. I am not using MergeDict because it will be deprecated --- social/strategies/django_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 0cc6a04d0..d0c439094 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -36,7 +36,7 @@ def request_data(self, merge=True): if not self.request: return {} if merge: - data = self.request.GET + data = self.request.GET.copy() data.update(self.request.POST) elif self.request.method == 'POST': data = self.request.POST From e1d891b1a27d8acf3abe84e7f58fd2823b9e2694 Mon Sep 17 00:00:00 2001 From: tell-k Date: Sun, 15 Feb 2015 02:33:16 +0900 Subject: [PATCH 466/890] Add dribble backend. --- docs/backends/dribbble.rst | 20 +++++++++ social/backends/dribbble.py | 61 ++++++++++++++++++++++++++ social/tests/backends/test_dribbble.py | 26 +++++++++++ 3 files changed, 107 insertions(+) create mode 100644 docs/backends/dribbble.rst create mode 100644 social/backends/dribbble.py create mode 100644 social/tests/backends/test_dribbble.py diff --git a/docs/backends/dribbble.rst b/docs/backends/dribbble.rst new file mode 100644 index 000000000..0bf2b99c4 --- /dev/null +++ b/docs/backends/dribbble.rst @@ -0,0 +1,20 @@ +Dribbble +======== + +Dribbble + +- Register a new application at `https://dribbble.com/account/applications/new`_, set the + callback URL to ``http://example.com/complete/dribbble/`` replacing + ``example.com`` with your domain. + +- Fill ``Client ID`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_DRIBBBLE_KEY = '' + SOCIAL_AUTH_DRIBBBLE_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_DRIBBBLE_SCOPE = [...] + + See auth scopes at http://developer.dribbble.com/v1/oauth/ + diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py new file mode 100644 index 000000000..28d84b2ec --- /dev/null +++ b/social/backends/dribbble.py @@ -0,0 +1,61 @@ +""" +Dribbble OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/dribbble.html +""" + +from social.backends.oauth import BaseOAuth2 + + +class DribbbleOAuth2(BaseOAuth2): + """Dribbble OAuth authentication backend""" + name = 'dribbble' + AUTHORIZATION_URL = 'https://dribbble.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://dribbble.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' + EXTRA_DATA = [ + ('id', 'id'), + ('name', 'name'), + ('html_url', 'html_url'), + ('avatar_url', 'avatar_url'), + ('bio', 'bio'), + ('location', 'location'), + ('links', 'links'), + ('buckets_count', 'buckets_count'), + ('comments_received_count', 'comments_received_count'), + ('followers_count', 'followers_count'), + ('followings_count', 'followings_count'), + ('likes_count', 'likes_count'), + ('likes_received_count', 'likes_received_count'), + ('projects_count', 'projects_count') + ('rebounds_received_count', 'rebounds_received_count'), + ('shots_count', 'shots_count'), + ('teams_count', 'teams_count'), + ('pro', 'pro'), + ('buckets_url', 'buckets_url'), + ('followers_url', 'followers_url'), + ('following_url', 'following_url'), + ('likes_url', 'shots_url'), + ('teams_url', 'teams_url'), + ('created_at', 'created_at'), + ('updated_at', 'updated_at'), + ] + + def get_user_details(self, response): + """Return user details from Dribbble account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) + return {'username': response.get('username'), + 'email': response.get('email', ''), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://api.dribbble.com/v1/user', + headers={ + 'Authorization': ' Bearer {0}'.format(access_token) + }) diff --git a/social/tests/backends/test_dribbble.py b/social/tests/backends/test_dribbble.py new file mode 100644 index 000000000..0abe86cc5 --- /dev/null +++ b/social/tests/backends/test_dribbble.py @@ -0,0 +1,26 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class DribbbleOAuth2Test(OAuth2Test): + backend_path = 'social.backends.dribbble.DribbbleOAuth2' + user_data_url = 'https://api.dribbble.com/v1/user' + expected_username = 'foobar' + + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + + user_data_body = json.dumps({ + 'id': 'foobar', + 'username': 'foobar', + 'name': 'Foo Bar' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 3108d6314eaefc7b5b57b14dd48ff9019096f3a8 Mon Sep 17 00:00:00 2001 From: tell-k Date: Sun, 15 Feb 2015 02:37:27 +0900 Subject: [PATCH 467/890] add document url. --- social/backends/dribbble.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py index 28d84b2ec..7aaf435f8 100644 --- a/social/backends/dribbble.py +++ b/social/backends/dribbble.py @@ -1,6 +1,7 @@ """ Dribbble OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/dribbble.html + http://developer.dribbble.com/v1/oauth/ """ from social.backends.oauth import BaseOAuth2 From 8d6ed64a8c78e612e137eae725a74a68c66aebbc Mon Sep 17 00:00:00 2001 From: tell-k Date: Sun, 15 Feb 2015 02:44:01 +0900 Subject: [PATCH 468/890] fixed bug. --- social/backends/dribbble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py index 7aaf435f8..e1c3eeeed 100644 --- a/social/backends/dribbble.py +++ b/social/backends/dribbble.py @@ -28,7 +28,7 @@ class DribbbleOAuth2(BaseOAuth2): ('followings_count', 'followings_count'), ('likes_count', 'likes_count'), ('likes_received_count', 'likes_received_count'), - ('projects_count', 'projects_count') + ('projects_count', 'projects_count'), ('rebounds_received_count', 'rebounds_received_count'), ('shots_count', 'shots_count'), ('teams_count', 'teams_count'), From 54310a5d4b8a0e6a40106ec3b47d20ad7c87fc7e Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Mon, 16 Feb 2015 19:13:43 +0000 Subject: [PATCH 469/890] Don't use "import" in example method paths docs to avoid confusion This is for two reasons: a) Using the verb "import" suggests that the module/function should be imported which is not true; we want a "dotted path notation" string rather than a reference to the method. b) "import" is an invalid module name anyway, so as an example of dotted path notation it is faulty. (Okay, it might actually be possible using `types` and `sys.modules` but.. yeah) Signed-off-by: Chris Lamb --- docs/pipeline.rst | 2 +- docs/use_cases.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 4e5b9acdf..64e65fb60 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -330,7 +330,7 @@ the pipeline, since it needs the user instance, it needs to be put after 'social.pipeline.social_auth.social_user', 'social.pipeline.user.get_username', 'social.pipeline.user.create_user', - 'import.path.to.save_profile', # <--- set the import-path to the function + 'path.to.save_profile', # <--- set the path to the function 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', 'social.pipeline.user.user_details' diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 3ba1cadb1..ac12fa269 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -287,7 +287,7 @@ Set this pipeline after ``social_user``:: 'social.pipeline.social_auth.social_uid', 'social.pipeline.social_auth.auth_allowed', 'social.pipeline.social_auth.social_user', - 'import.path.to.redirect_if_no_refresh_token', + 'path.to.redirect_if_no_refresh_token', 'social.pipeline.user.get_username', 'social.pipeline.user.create_user', 'social.pipeline.social_auth.associate_user', From 97d9c5d2a242b23511ec63a2139ddd53fffa6449 Mon Sep 17 00:00:00 2001 From: Sergey Kozub Date: Wed, 18 Feb 2015 00:45:19 +0300 Subject: [PATCH 470/890] fix python3 handling of openid backend on sqlalchemy storage (use str instead of bytes) --- social/storage/sqlalchemy_orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 0947792c0..e7182cee8 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -181,7 +181,7 @@ def store(cls, server_url, association): except IndexError: assoc = cls(server_url=server_url, handle=association.handle) - assoc.secret = base64.encodestring(association.secret) + assoc.secret = base64.encodestring(association.secret).decode() assoc.issued = association.issued assoc.lifetime = association.lifetime assoc.assoc_type = association.assoc_type From f229c075c98f658a9c1d62595c6278173b1ce104 Mon Sep 17 00:00:00 2001 From: Motoki Naruse Date: Sat, 21 Feb 2015 14:16:48 +0900 Subject: [PATCH 471/890] Email column is duplicated --- examples/pyramid_example/example/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/pyramid_example/example/models.py b/examples/pyramid_example/example/models.py index 1c7f6d6b4..03b609886 100644 --- a/examples/pyramid_example/example/models.py +++ b/examples/pyramid_example/example/models.py @@ -16,5 +16,4 @@ class User(Base): email = Column(String(200)) password = Column(String(200), default='') name = Column(String(100)) - email = Column(String(200)) active = Column(Boolean, default=True) From bbf02d71817365f77f9c87d0360e0639f9c61313 Mon Sep 17 00:00:00 2001 From: Motoki Naruse Date: Sat, 21 Feb 2015 15:19:42 +0900 Subject: [PATCH 472/890] Include template engine I got ``` builtins.ValueError ValueError: No such renderer factory .pt ``` --- examples/pyramid_example/example/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pyramid_example/example/__init__.py b/examples/pyramid_example/example/__init__.py index 549ee32fe..bc0e6b7e8 100644 --- a/examples/pyramid_example/example/__init__.py +++ b/examples/pyramid_example/example/__init__.py @@ -20,6 +20,7 @@ def main(global_config, **settings): config = Configurator(settings=settings, session_factory=session_factory, autocommit=True) + config.include('pyramid_chameleon') config.add_static_view('static', 'static', cache_max_age=3600) config.add_request_method('example.auth.get_user', 'user', reify=True) config.add_route('home', '/') From 2b6d9696425eb3c176470e45785f50a3fee575e4 Mon Sep 17 00:00:00 2001 From: Motoki Naruse Date: Sat, 21 Feb 2015 16:55:20 +0900 Subject: [PATCH 473/890] login_user takes 3 parameters And `strategy.request` is function. I don't know this is depending on environment or is not depending on environment. I'm useing * Python 3.4.0 * Pyramid 1.5.2 --- examples/pyramid_example/example/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pyramid_example/example/auth.py b/examples/pyramid_example/example/auth.py index 831de56cc..98526f2a5 100644 --- a/examples/pyramid_example/example/auth.py +++ b/examples/pyramid_example/example/auth.py @@ -5,8 +5,8 @@ from example.models import DBSession, User -def login_user(strategy, user): - strategy.request.session['user_id'] = user.id +def login_user(strategy, user, user_social_auth): + strategy.strategy.session_set('user_id', user.id) def login_required(request): From 49c8f8388eb409aad5187e1654cceccef067eca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 23 Feb 2015 12:12:53 -0200 Subject: [PATCH 474/890] PEP8/PyFlakes --- social/backends/zotero.py | 6 +----- social/tests/backends/test_zotero.py | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/social/backends/zotero.py b/social/backends/zotero.py index 427e74b4b..83d494be5 100644 --- a/social/backends/zotero.py +++ b/social/backends/zotero.py @@ -2,11 +2,7 @@ Zotero OAuth1 backends, docs at: http://psa.matiasaguirre.net/docs/backends/zotero.html """ -from requests import HTTPError - -from social.backends.oauth import BaseOAuth2, BaseOAuth1 -from social.exceptions import AuthMissingParameter, AuthCanceled -import ipdb +from social.backends.oauth import BaseOAuth1 class ZoteroOAuth(BaseOAuth1): diff --git a/social/tests/backends/test_zotero.py b/social/tests/backends/test_zotero.py index d9b837d2e..f9507606a 100644 --- a/social/tests/backends/test_zotero.py +++ b/social/tests/backends/test_zotero.py @@ -7,8 +7,6 @@ class ZoteroOAuth1Test(OAuth1Test): backend_path = 'social.backends.zotero.ZoteroOAuth' expected_username = 'FooBar' - - access_token_body = json.dumps({ 'access_token': {u'oauth_token': u'foobar', u'oauth_token_secret': u'rodgsNDK4hLJU1504Atk131G', From 8561dc45680d00a683fa1d91c5ecf63df60e42c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 23 Feb 2015 13:07:16 -0200 Subject: [PATCH 475/890] v0.2.2 --- .gitignore | 3 +- Changelog | 243 ++++++++++++++++++++++++++++++++++++++++++++- social/__init__.py | 2 +- 3 files changed, 244 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index bf45b883b..cf1280d36 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,6 @@ local_settings.py sessions/ _build/ fabfile.py +changelog.sh -.DS_Store \ No newline at end of file +.DS_Store diff --git a/Changelog b/Changelog index d937b10dc..fc0446cfc 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,244 @@ -2014-11-01 HEAD (unreleased) -============================ +2015-02-23 v0.2.2 +================= + + * 2015-02-23 Matías Aguirre + PEP8/PyFlakes + + * 2015-02-21 Motoki Naruse + login_user takes 3 parameters + + * 2015-02-21 Motoki Naruse + Include template engine + + * 2015-02-21 Motoki Naruse + Email column is duplicated + + * 2015-02-18 Sergey Kozub + fix python3 handling of openid backend on sqlalchemy storage (use str + instead of bytes) + + * 2015-02-16 Chris Lamb + Don't use "import" in example method paths docs to avoid confusion + + * 2015-02-15 tell-k + fixed bug. + + * 2015-02-15 tell-k + add document url. + + * 2015-02-15 tell-k + Add dribble backend. + + * 2015-02-14 Alejandro Baronetti + Fixed issue: GET dictionary is immutable. I am not using MergeDict because + it will be deprecated + + * 2015-02-13 Chris Martin + Include username in Reddit extra_data + + * 2015-02-11 tell-k + refs #512 fixed typo + + * 2015-02-11 tell-k + refs #512 fixed bug for py2.6 + + * 2015-02-11 tell-k + add qiita backend + + * 2015-02-10 Matías Aguirre + Pyflakes + + * 2015-02-10 Matías Aguirre + PEP8 + + * 2015-02-07 Alejandro Baronetti + Fix: REQUEST has been deprecated in Django 1.7, so we need to merge + dictionaries + + * 2015-02-06 Clinton Blackburn + Updated PyJWT Dependency + + * 2015-02-06 Rafael Muñoz Cárdenas + Update Google documentation + + * 2015-02-03 Matías Aguirre + Add test for nationbuilder backend + + * 2015-02-03 Matías Aguirre + Move common code to base class + + * 2015-02-03 Matías Aguirre + NationBuilder backend + + * 2015-02-03 Matías Aguirre + Define methods to customize urls in OAuth2 backends + + * 2015-02-03 Matías Aguirre + Enable debug pipeline in example app + + * 2015-02-03 Ian Wienand + Ensure email is not None + + * 2015-02-02 Chris DeBlois + updated mendeley oauth2 to use new api resource and also updated to grab + new profile_id, name and bio + + * 2015-02-02 Ian Wienand + Add support for Launchpad OpenId + + * 2015-01-30 rivf + Fixed jawbone authentification + + * 2015-01-27 Adam Babik + Added coursera backend to README + + * 2015-01-27 Adam Babik + Docs for coursera backend + + * 2015-01-23 Adam Babik + Added Coursera backend to django_example + + * 2015-01-23 Adam Babik + Added backend for Coursera + + * 2015-01-20 ayush + Added nonce unique constraint + + * 2015-01-19 Matías Aguirre + Patch tornado arguments/cookies getting. Refs #445. Refs #346 + + * 2015-01-10 Chris Barna + Store Spotify's refresh_token. + + * 2015-01-07 Nick Sullivan + cleanly handle both a scope of 'identity' only and also fill in more data + if we have 'read' access + + * 2015-01-07 Nick Sullivan + properly handle data, so that it is more future proof, again. This time fix + issue with team_url + + * 2015-01-07 Nick Sullivan + update in a way that will be more future proof + + * 2015-01-07 Nick Sullivan + when scope is reduced, the response from slack is different, handle both + + * 2015-01-05 Ben Davis + Fixed extra_data field in django 1.7 initial migration + + * 2015-01-02 Jun Wang + Fix YahooOAuth get primary email sorting order + + * 2015-01-02 Matías Aguirre + PEP8 + + * 2015-01-02 Matías Aguirre + Link docs, apply PEP8, change quotes and code style + + * 2015-01-02 Matías Aguirre + Change expression + + * 2015-01-02 travoltino + Update base.py + + * 2014-12-29 Alex Muller + Correct Django SESSION_COOKIE_AGE setting + + * 2014-12-28 Nick Sullivan + Documentation for slack backend + + * 2014-12-28 Nick Sullivan + Slack backend + + * 2014-12-24 Alex Muller + Update GitHub documentation + + * 2014-11-27 James Potter + Update django.rst + + * 2014-11-26 Sasha Golubev + Added backend for professionali.ru + + * 2014-11-24 Lukas Klein + Removed Orkut backend + + * 2014-11-24 Matías Aguirre + Remove Flask-SQLAlchemy dependency from example app + + * 2014-11-23 Matías Aguirre + Simplify flask app initialization + + * 2014-11-22 Matías Aguirre + Allow initial definition of protected attributes + + * 2014-11-22 Matías Aguirre + Quick khan academy docs + + * 2014-11-22 Matías Aguirre + PEP8 + + * 2014-11-22 Matías Aguirre + PEP8 + + * 2014-11-21 tschilling + Allow the pipeline to change the redirect url. Moves the popping of the + redirect value from the session to after the pipe line executes. + + * 2014-11-19 Seán Hayes + Added support for Django's User.EMAIL_FIELD. + + * 2014-11-18 Anna Warzecha + Changed test name + + * 2014-11-18 Anna Warzecha + Fixed docs + + * 2014-11-18 Anna Warzecha + Khan Academy oauth support now fully working + + * 2014-11-18 Anna Warzecha + Struggling with Khan Academy again... + + * 2014-11-16 Anna Warzecha + Fix backend name formatting + + * 2014-11-16 Anna Warzecha + Fix readme + + * 2014-11-16 Anna Warzecha + Basic Khan Academy support + + * 2014-11-15 Matías Aguirre + Avoid override of custom-usernames fields with plain generated username. + Refs #435 + + * 2014-11-15 Matías Aguirre + Fix docs. Refs #436 + + * 2014-11-14 Matías Aguirre + Remove x flag from .py file + + * 2014-11-11 Matías Aguirre + Example on how to re-prompt a google user to get the refresh_token + + * 2014-11-07 Miguel Paolino + Added zotero test, work in progress + + * 2014-11-07 Miguel Paolino + Udpated README to include the Zotero backend mention + + * 2014-11-07 Miguel Paolino + Fixed doc line + + * 2014-11-07 Miguel Paolino + Added Zotero OAuth1 backend + + * 2014-11-02 Matías Aguirre + Pass request to pyramid strategy. Refs #390 + + * 2014-11-02 Matías Aguirre + Update changelog. Refs #421 * 2014-11-01 Matías Aguirre PEP8 diff --git a/social/__init__.py b/social/__init__.py index d5d5847c9..7df9523d1 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 2) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 29bb6ed60c37c56f7f31babc4444e33dff17be30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 23 Feb 2015 13:54:08 -0200 Subject: [PATCH 476/890] Fix zotero tests --- social/backends/zotero.py | 8 +++++--- social/tests/backends/test_zotero.py | 14 ++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/social/backends/zotero.py b/social/backends/zotero.py index 83d494be5..3e544846b 100644 --- a/social/backends/zotero.py +++ b/social/backends/zotero.py @@ -22,6 +22,8 @@ def get_user_id(self, details, response): def get_user_details(self, response): """Return user details from Zotero API account""" - access_token = response.get('access_token', dict()) - return {'username': access_token.get('username', ''), - 'userID': access_token.get('userID', '')} + access_token = response.get('access_token', {}) + return { + 'username': access_token.get('username', ''), + 'userID': access_token.get('userID', '') + } diff --git a/social/tests/backends/test_zotero.py b/social/tests/backends/test_zotero.py index f9507606a..e93aa0f5c 100644 --- a/social/tests/backends/test_zotero.py +++ b/social/tests/backends/test_zotero.py @@ -1,5 +1,3 @@ -import json - from social.p3 import urlencode from social.tests.backends.oauth import OAuth1Test @@ -7,12 +5,12 @@ class ZoteroOAuth1Test(OAuth1Test): backend_path = 'social.backends.zotero.ZoteroOAuth' expected_username = 'FooBar' - access_token_body = json.dumps({ - 'access_token': {u'oauth_token': u'foobar', - u'oauth_token_secret': u'rodgsNDK4hLJU1504Atk131G', - u'userID': u'123456_abcdef', - u'username': u'anyusername'}}) - + access_token_body = urlencode({ + 'oauth_token': 'foobar', + 'oauth_token_secret': 'rodgsNDK4hLJU1504Atk131G', + 'userID': '123456_abcdef', + 'username': 'FooBar' + }) request_token_body = urlencode({ 'oauth_token_secret': 'foobar-secret', 'oauth_token': 'foobar', From c33c94761e7cf5ac5936abf5d7f16ce74d627f61 Mon Sep 17 00:00:00 2001 From: zz Date: Tue, 24 Feb 2015 20:18:44 +0800 Subject: [PATCH 477/890] Fix Issue #532, get UID when use access_token ajax auth in weibo backends. --- social/backends/weibo.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/social/backends/weibo.py b/social/backends/weibo.py index 6082ae9d3..0bda1b55e 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -5,7 +5,7 @@ http://psa.matiasaguirre.net/docs/backends/weibo.html """ from social.backends.oauth import BaseOAuth2 - +import json class WeiboOAuth2(BaseOAuth2): """Weibo (of sina) OAuth authentication backend""" @@ -14,6 +14,7 @@ class WeiboOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://api.weibo.com/oauth2/authorize' REQUEST_TOKEN_URL = 'https://api.weibo.com/oauth2/request_token' ACCESS_TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' + ACCESS_TOKEN_INFO_URL = 'https://api.weibo.com/oauth2/get_token_info' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False EXTRA_DATA = [ @@ -39,7 +40,28 @@ def get_user_details(self, response): 'first_name': first_name, 'last_name': last_name} + def get_uid(self, access_token): + """ return uid by access_token""" + + response = self.request(self.ACCESS_TOKEN_INFO_URL, + method='POST', + params={'access_token':access_token}) + + data = response.json() + return data['uid'] + def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://api.weibo.com/2/users/show.json', - params={'access_token': access_token, - 'uid': kwargs['response']['uid']}) + """ if use access_token for ajax auth, then would raise KeyError + because there is no uid in response, so must get uid. + """ + if 'response' not in kwargs or 'uid' not in kwargs['response']: + uid = self.get_uid(access_token) + response = kwargs.setdefault('response', {}) + response['uid'] = uid + + response = self.get_json('https://api.weibo.com/2/users/show.json', + params={'access_token': access_token, + 'uid': kwargs['response']['uid']}) + + response['uid'] = kwargs['response']['uid'] + return response \ No newline at end of file From c60549bf859829178b8f4516c0060d5ecaf8554c Mon Sep 17 00:00:00 2001 From: zz Date: Tue, 24 Feb 2015 20:23:06 +0800 Subject: [PATCH 478/890] Fix Issue #532, get UID when use access_token ajax auth in weibo backends. --- social/backends/weibo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/weibo.py b/social/backends/weibo.py index 0bda1b55e..ddf884e6b 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -5,7 +5,7 @@ http://psa.matiasaguirre.net/docs/backends/weibo.html """ from social.backends.oauth import BaseOAuth2 -import json + class WeiboOAuth2(BaseOAuth2): """Weibo (of sina) OAuth authentication backend""" From 53091fcca5ef0d74b8ec234dd963f0bba1ba5145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 24 Feb 2015 14:48:43 -0200 Subject: [PATCH 479/890] Cleanup imports and hmac creation, fix python3 compatibility --- social/backends/facebook.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index eb7a494b6..17b523818 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -13,11 +13,6 @@ from social.exceptions import AuthException, AuthCanceled, AuthUnknownError, \ AuthMissingParameter -import hmac -import hashlib - -def hmac_sha256(key, msg): - return hmac.new(key, msg, digestmod=hashlib.sha256).hexdigest() class FacebookOAuth2(BaseOAuth2): """Facebook OAuth2 authentication backend""" @@ -54,8 +49,11 @@ def user_data(self, access_token, *args, **kwargs): if self.setting('APPSECRET_PROOF', True): _, secret = self.get_key_and_secret() - params['appsecret_proof'] = hmac_sha256(secret, access_token); - + params['appsecret_proof'] = hmac.new( + secret.encode('utf8'), + msg=access_token.encode('utf8'), + digestmod=hashlib.sha256 + ).hexdigest() return self.get_json(self.USER_DATA_URL, params=params) def process_error(self, data): From 7094f806f31785899c1177f43895f48b5ab34a24 Mon Sep 17 00:00:00 2001 From: Hassek Date: Wed, 25 Feb 2015 13:27:27 -0430 Subject: [PATCH 480/890] added OAuth2 support to yahoo. Also, removed OAuth1 since yahoo will not be supporting it anymore --- social/backends/yahoo.py | 83 +++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 206949e04..872d8a874 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -1,9 +1,13 @@ """ -Yahoo OpenId and OAuth1 backends, docs at: +Yahoo OpenId, OAuth2 backends, docs at: http://psa.matiasaguirre.net/docs/backends/yahoo.html """ +from requests import HTTPError +from requests.auth import HTTPBasicAuth + from social.backends.open_id import OpenIdAuth -from social.backends.oauth import BaseOAuth1 +from social.backends.oauth import BaseOAuth2 +from social.exceptions import AuthCanceled, AuthUnknownError class YahooOpenId(OpenIdAuth): @@ -12,20 +16,26 @@ class YahooOpenId(OpenIdAuth): URL = 'http://me.yahoo.com' -class YahooOAuth(BaseOAuth1): - """Yahoo OAuth authentication backend""" - name = 'yahoo-oauth' +class YahooOAuth2(BaseOAuth2): + """Yahoo OAuth2 authentication backend""" + name = 'yahoo-oauth2' ID_KEY = 'guid' - AUTHORIZATION_URL = 'https://api.login.yahoo.com/oauth/v2/request_auth' - REQUEST_TOKEN_URL = \ - 'https://api.login.yahoo.com/oauth/v2/get_request_token' - ACCESS_TOKEN_URL = 'https://api.login.yahoo.com/oauth/v2/get_token' + AUTHORIZATION_URL = 'https://api.login.yahoo.com/oauth2/request_auth' + ACCESS_TOKEN_URL = 'https://api.login.yahoo.com/oauth2/get_token' + ACCESS_TOKEN_METHOD = 'POST' EXTRA_DATA = [ - ('guid', 'id'), + ('xoauth_yahoo_guid', 'id'), ('access_token', 'access_token'), - ('expires', 'expires') + ('expires_in', 'expires'), + ('refresh_token', 'refresh_token'), + ('token_type', 'token_type'), ] + def get_user_names(self, first_name, last_name): + if first_name or last_name: + return " ".join((first_name, last_name)), first_name, last_name + return None, None, None + def get_user_details(self, response): """Return user details from Yahoo Profile""" fullname, first_name, last_name = self.get_user_names( @@ -36,25 +46,44 @@ def get_user_details(self, response): if email.get('handle')] emails.sort(key=lambda e: e.get('primary', False), reverse=True) return {'username': response.get('nickname'), - 'email': emails[0]['handle'] if emails else '', + 'email': emails[0]['handle'] if emails else response.get('guid', ''), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json' - return self.get_json( - url.format(self._get_guid(access_token)), - auth=self.oauth_auth(access_token) - )['profile'] - - def _get_guid(self, access_token): - """ - Beause you have to provide GUID for every API request - it's also returned during one of OAuth calls - """ - return self.get_json( - 'https://social.yahooapis.com/v1/me/guid?format=json', - auth=self.oauth_auth(access_token) - )['guid']['value'] + url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json'.format( + kwargs['response']['xoauth_yahoo_guid']) + return self.get_json(url, headers={'Authorization': 'Bearer {0}'.format( + access_token)}, method='GET')['profile'] + + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + self.process_error(self.data) + client_id, client_secret = self.get_key_and_secret() + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + auth=HTTPBasicAuth(client_id, client_secret), + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + raise + except KeyError: + raise AuthUnknownError(self) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) + + def auth_complete_params(self, state=None): + return { + 'grant_type': 'authorization_code', # request auth code + 'code': self.data.get('code', ''), # server response code + 'redirect_uri': self.get_redirect_uri(state) + } From d9293d19b0ceab417a66c31fa6fb41d8c9c4d4cf Mon Sep 17 00:00:00 2001 From: Hassek Date: Wed, 25 Feb 2015 14:49:33 -0430 Subject: [PATCH 481/890] fixed refresh tokens for yahoo --- social/backends/yahoo.py | 73 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 872d8a874..242b0963c 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -1,12 +1,12 @@ """ -Yahoo OpenId, OAuth2 backends, docs at: +Yahoo OpenId, OAuth1 and OAuth2 backends, docs at: http://psa.matiasaguirre.net/docs/backends/yahoo.html """ from requests import HTTPError from requests.auth import HTTPBasicAuth from social.backends.open_id import OpenIdAuth -from social.backends.oauth import BaseOAuth2 +from social.backends.oauth import BaseOAuth2, BaseOAuth1 from social.exceptions import AuthCanceled, AuthUnknownError @@ -16,6 +16,54 @@ class YahooOpenId(OpenIdAuth): URL = 'http://me.yahoo.com' +class YahooOAuth(BaseOAuth1): + """Yahoo OAuth authentication backend""" + name = 'yahoo-oauth' + ID_KEY = 'guid' + AUTHORIZATION_URL = 'https://api.login.yahoo.com/oauth/v2/request_auth' + REQUEST_TOKEN_URL = \ + 'https://api.login.yahoo.com/oauth/v2/get_request_token' + ACCESS_TOKEN_URL = 'https://api.login.yahoo.com/oauth/v2/get_token' + EXTRA_DATA = [ + ('guid', 'id'), + ('access_token', 'access_token'), + ('expires', 'expires') + ] + + def get_user_details(self, response): + """Return user details from Yahoo Profile""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('givenName'), + last_name=response.get('familyName') + ) + emails = [email for email in response.get('emails', []) + if email.get('handle')] + emails.sort(key=lambda e: e.get('primary', False), reverse=True) + return {'username': response.get('nickname'), + 'email': emails[0]['handle'] if emails else '', + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json' + return self.get_json( + url.format(self._get_guid(access_token)), + auth=self.oauth_auth(access_token) + )['profile'] + + def _get_guid(self, access_token): + """ + Beause you have to provide GUID for every API request + it's also returned during one of OAuth calls + """ + return self.get_json( + 'https://social.yahooapis.com/v1/me/guid?format=json', + auth=self.oauth_auth(access_token) + )['guid']['value'] + + class YahooOAuth2(BaseOAuth2): """Yahoo OAuth2 authentication backend""" name = 'yahoo-oauth2' @@ -61,11 +109,10 @@ def user_data(self, access_token, *args, **kwargs): def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - client_id, client_secret = self.get_key_and_secret() try: response = self.request_access_token( self.ACCESS_TOKEN_URL, - auth=HTTPBasicAuth(client_id, client_secret), + auth=HTTPBasicAuth(*self.get_key_and_secret()), data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD @@ -81,6 +128,24 @@ def auth_complete(self, *args, **kwargs): return self.do_auth(response['access_token'], response=response, *args, **kwargs) + def refresh_token_params(self, token, *args, **kwargs): + return { + 'refresh_token': token, + 'grant_type': 'refresh_token', + 'redirect_uri': 'oob', # out of bounds + } + + def refresh_token(self, token, *args, **kwargs): + params = self.refresh_token_params(token, *args, **kwargs) + url = self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL + method = self.REFRESH_TOKEN_METHOD + key = 'params' if method == 'GET' else 'data' + request_args = {'headers': self.auth_headers(), + 'method': method, + key: params} + request = self.request(url, auth=HTTPBasicAuth(*self.get_key_and_secret()), **request_args) + return self.process_refresh_token_response(request, *args, **kwargs) + def auth_complete_params(self, state=None): return { 'grant_type': 'authorization_code', # request auth code From e184646beabb4ac0499bd49509d1e764d026918d Mon Sep 17 00:00:00 2001 From: Hassek Date: Mon, 2 Mar 2015 11:31:36 -0430 Subject: [PATCH 482/890] modified docs --- README.rst | 2 +- docs/backends/yahoo.rst | 13 +++++-------- social/backends/yahoo.py | 7 +++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 1ce85b9c2..11ee81cce 100644 --- a/README.rst +++ b/README.rst @@ -118,7 +118,7 @@ or current ones extended): * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 * Xing_ OAuth1 - * Yahoo_ OpenId and OAuth1 + * Yahoo_ OpenId and OAuth2 * Yammer_ OAuth2 * Yandex_ OAuth1, OAuth2 and OpenId * Zotero_ OAuth1 diff --git a/docs/backends/yahoo.rst b/docs/backends/yahoo.rst index b0919a484..ca002f694 100644 --- a/docs/backends/yahoo.rst +++ b/docs/backends/yahoo.rst @@ -1,7 +1,7 @@ Yahoo ===== -Yahoo supports OpenId and OAuth1 for their auth flow. +Yahoo supports OpenId and OAuth2 for their auth flow. Yahoo OpenId @@ -17,17 +17,14 @@ in the ``AUTHENTICATION_BACKENDS`` setting:: ) -Yahoo OAuth1 +Yahoo OAuth2 ------------ - -OAuth 1.0 workflow, useful if you are planning to use Yahoo's API. +OAuth 2.0 workflow, useful if you are planning to use Yahoo's API. - Register a new application at `Yahoo Developer Center`_, set your app domain and configure scopes (they can't be overriden by application). - Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - SOCIAL_AUTH_YAHOO_OAUTH_KEY = '' - SOCIAL_AUTH_YAHOO_OAUTH_SECRET = '' - -.. _Yahoo Developer Center: https://developer.apps.yahoo.com/projects/ + SOCIAL_AUTH_YAHOO_OAUTH2_KEY = '' + SOCIAL_AUTH_YAHOO_OAUTH2_SECRET = '' diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 242b0963c..d156c324c 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -17,7 +17,7 @@ class YahooOpenId(OpenIdAuth): class YahooOAuth(BaseOAuth1): - """Yahoo OAuth authentication backend""" + """Yahoo OAuth authentication backend. DEPRECATED""" name = 'yahoo-oauth' ID_KEY = 'guid' AUTHORIZATION_URL = 'https://api.login.yahoo.com/oauth/v2/request_auth' @@ -85,7 +85,10 @@ def get_user_names(self, first_name, last_name): return None, None, None def get_user_details(self, response): - """Return user details from Yahoo Profile""" + """ + Return user details from Yahoo Profile. + To Get user email you need the profile private read permission. + """ fullname, first_name, last_name = self.get_user_names( first_name=response.get('givenName'), last_name=response.get('familyName') From 81f4116c95c9c533a1f325bd5103d205c249e1fc Mon Sep 17 00:00:00 2001 From: Tom Clancy Date: Mon, 2 Mar 2015 16:12:18 -0500 Subject: [PATCH 483/890] Update google.rst Added note about turning on Google+ API since the Google docs aren't clear on this and the error message doesn't bubble up to the front-end (just shows as a 403 which looks like a CSRF failure). --- docs/backends/google.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 1ef2dce52..9edf22d29 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -64,7 +64,7 @@ done by their Javascript which thens calls a defined handler to complete the auth process. * To enable the backend create an application using the `Google console`_ and - following the steps from the `official guide`_. + following the steps from the `official guide`_. Make sure to enable the Google+ API in the console. * Fill in the key settings looking inside the Google console the subsection ``Credentials`` inside ``API & auth``:: From 5b6e3ebd4adc79e6d2a23f3e0f5e4593a7f41b3b Mon Sep 17 00:00:00 2001 From: dobestan Date: Tue, 3 Mar 2015 17:20:54 +0900 Subject: [PATCH 484/890] Disable redirect_state in kakao backend. Fixes #538 --- social/backends/kakao.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/kakao.py b/social/backends/kakao.py index b20cdf956..0c60bfc38 100644 --- a/social/backends/kakao.py +++ b/social/backends/kakao.py @@ -11,6 +11,7 @@ class KakaoOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://kauth.kakao.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://kauth.kakao.com/oauth/token' ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False def get_user_id(self, details, response): return response['id'] From f0749ee60b707c2f8c1452b87c754b2e383fb3ed Mon Sep 17 00:00:00 2001 From: dobestan Date: Tue, 3 Mar 2015 17:21:59 +0900 Subject: [PATCH 485/890] Enable KakaoOAuth2 on example app --- examples/django_example/example/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 295dc0e4b..6747846a6 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -151,6 +151,7 @@ 'social.backends.google.GoogleOpenIdConnect', 'social.backends.instagram.InstagramOAuth2', 'social.backends.jawbone.JawboneOAuth2', + 'social.backends.kakao.KakaoOAuth2', 'social.backends.linkedin.LinkedinOAuth', 'social.backends.linkedin.LinkedinOAuth2', 'social.backends.live.LiveOAuth2', From 26cd45541aeb99df9ba9f879fefa863d29fbeabc Mon Sep 17 00:00:00 2001 From: dobestan Date: Tue, 3 Mar 2015 21:38:05 +0900 Subject: [PATCH 486/890] update Kakao OAuth2 backend : update auth process- Fixes #538 --- social/backends/kakao.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/social/backends/kakao.py b/social/backends/kakao.py index 0c60bfc38..279a5aa1e 100644 --- a/social/backends/kakao.py +++ b/social/backends/kakao.py @@ -31,3 +31,10 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://kapi.kakao.com/v1/user/me', params={'access_token': access_token}) + + def auth_complete_params(self, state=None): + return { + 'grant_type': 'authorization_code', + 'code': self.data.get('code', ''), + 'client_id': self.get_key_and_secret()[0], + } From 93d1b61f41ef355895880dbd4750b4b7f5fc6d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mu=C3=B1oz=20C=C3=A1rdenas?= Date: Thu, 5 Mar 2015 15:40:20 +0100 Subject: [PATCH 487/890] Add extra info on Google+ Sign-In doc --- docs/backends/google.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 1ef2dce52..0bdfb8ef0 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -104,6 +104,10 @@ auth process. ``plus_id`` is the value from ``SOCIAL_AUTH_GOOGLE_PLUS_KEY``. ``signInCallback`` is the name of your Javascript callback function. + If you would like to get user's email address and have it stored, then set + this value in `data-scope`:: + + data-scope="https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email" * Add the Javascript snippet in the same template as above:: From 23ab544dae8541143e7a6efb0858b5b88e3550b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 5 Mar 2015 13:44:30 -0200 Subject: [PATCH 488/890] PEP8 --- social/backends/yahoo.py | 48 ++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index d156c324c..7ab046db5 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -55,8 +55,8 @@ def user_data(self, access_token, *args, **kwargs): def _get_guid(self, access_token): """ - Beause you have to provide GUID for every API request - it's also returned during one of OAuth calls + Beause you have to provide GUID for every API request it's also + returned during one of OAuth calls """ return self.get_json( 'https://social.yahooapis.com/v1/me/guid?format=json', @@ -81,33 +81,37 @@ class YahooOAuth2(BaseOAuth2): def get_user_names(self, first_name, last_name): if first_name or last_name: - return " ".join((first_name, last_name)), first_name, last_name + return ' '.join((first_name, last_name)), first_name, last_name return None, None, None def get_user_details(self, response): """ - Return user details from Yahoo Profile. - To Get user email you need the profile private read permission. + Return user details from Yahoo Profile. + To Get user email you need the profile private read permission. """ fullname, first_name, last_name = self.get_user_names( first_name=response.get('givenName'), last_name=response.get('familyName') ) emails = [email for email in response.get('emails', []) - if email.get('handle')] + if 'handle' in email] emails.sort(key=lambda e: e.get('primary', False), reverse=True) - return {'username': response.get('nickname'), - 'email': emails[0]['handle'] if emails else response.get('guid', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} + email = emails[0]['handle'] if emails else response.get('guid', '') + return { + 'username': response.get('nickname'), + 'email': email, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name + } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json'.format( - kwargs['response']['xoauth_yahoo_guid']) - return self.get_json(url, headers={'Authorization': 'Bearer {0}'.format( - access_token)}, method='GET')['profile'] + url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json' \ + .format(kwargs['response']['xoauth_yahoo_guid']) + return self.get_json(url, headers={ + 'Authorization': 'Bearer {0}'.format(access_token) + }, method='GET')['profile'] def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" @@ -143,10 +147,16 @@ def refresh_token(self, token, *args, **kwargs): url = self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL method = self.REFRESH_TOKEN_METHOD key = 'params' if method == 'GET' else 'data' - request_args = {'headers': self.auth_headers(), - 'method': method, - key: params} - request = self.request(url, auth=HTTPBasicAuth(*self.get_key_and_secret()), **request_args) + request_args = { + 'headers': self.auth_headers(), + 'method': method, + key: params + } + request = self.request( + url, + auth=HTTPBasicAuth(*self.get_key_and_secret()), + **request_args + ) return self.process_refresh_token_response(request, *args, **kwargs) def auth_complete_params(self, state=None): From c768998b987688fe0e5673580ef93b2d47440491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 5 Mar 2015 13:57:11 -0200 Subject: [PATCH 489/890] PEP8 and simplify code --- social/backends/weibo.py | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/social/backends/weibo.py b/social/backends/weibo.py index ddf884e6b..25b60a94f 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -14,7 +14,6 @@ class WeiboOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://api.weibo.com/oauth2/authorize' REQUEST_TOKEN_URL = 'https://api.weibo.com/oauth2/request_token' ACCESS_TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' - ACCESS_TOKEN_INFO_URL = 'https://api.weibo.com/oauth2/get_token_info' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False EXTRA_DATA = [ @@ -41,27 +40,22 @@ def get_user_details(self, response): 'last_name': last_name} def get_uid(self, access_token): - """ return uid by access_token""" - - response = self.request(self.ACCESS_TOKEN_INFO_URL, - method='POST', - params={'access_token':access_token}) - - data = response.json() + """Return uid by access_token""" + data = self.get_json( + 'https://api.weibo.com/oauth2/get_token_info', + method='POST', + params={'access_token': access_token} + ) return data['uid'] - def user_data(self, access_token, *args, **kwargs): - """ if use access_token for ajax auth, then would raise KeyError - because there is no uid in response, so must get uid. - """ - if 'response' not in kwargs or 'uid' not in kwargs['response']: - uid = self.get_uid(access_token) - response = kwargs.setdefault('response', {}) - response['uid'] = uid - - response = self.get_json('https://api.weibo.com/2/users/show.json', - params={'access_token': access_token, - 'uid': kwargs['response']['uid']}) - - response['uid'] = kwargs['response']['uid'] - return response \ No newline at end of file + def user_data(self, access_token, response=None, *args, **kwargs): + """Return user data""" + # If user id was not retrieved in the response, then get it directly + # from weibo get_token_info endpoint + uid = response and response.get('uid') or self.get_uid(access_token) + user_data = self.get_json( + 'https://api.weibo.com/2/users/show.json', + params={'access_token': access_token, 'uid': uid} + ) + user_data['uid'] = uid + return user_data From e27ca9e2b3c860420450046236851a82aaf6dac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20E=C3=9Fer?= Date: Fri, 6 Mar 2015 18:20:53 +0100 Subject: [PATCH 490/890] Add backend for EVE Online Single Sign-On (OAuth2) https://developers.eveonline.com/resource/single-sign-on --- docs/backends/eveonline.rst | 21 ++++++++++ .../local_settings.py.template | 1 + examples/django_example/example/settings.py | 1 + .../django_me_example/example/settings.py | 1 + examples/flask_example/settings.py | 1 + examples/flask_me_example/settings.py | 1 + examples/pyramid_example/example/settings.py | 1 + examples/tornado_example/settings.py | 1 + examples/webpy_example/app.py | 1 + social/backends/eveonline.py | 39 +++++++++++++++++++ 10 files changed, 68 insertions(+) create mode 100644 docs/backends/eveonline.rst create mode 100644 social/backends/eveonline.py diff --git a/docs/backends/eveonline.rst b/docs/backends/eveonline.rst new file mode 100644 index 000000000..8a85a82f6 --- /dev/null +++ b/docs/backends/eveonline.rst @@ -0,0 +1,21 @@ +EVE Online Single Sign-On (SSO) +=============================== + +The EVE Single Sign-On (SSO) works similar to GitHub (OAuth2). + +- Register a new application at `EVE Developers`_, set the callback URL to + ``http://example.com/complete/eveonline/`` replacing ``example.com`` with your + domain. + +- Fill the ``Client ID`` and ``Secret Key`` values from EVE Developers in the settings:: + + SOCIAL_AUTH_EVEONLINE_KEY = '' + SOCIAL_AUTH_EVEONLINE_SECRET = '' + +- If you want to use EVE Character names as user names, use this setting:: + + SOCIAL_AUTH_CLEAN_USERNAMES = False + +- If you want to access EVE Online's CREST API, use:: + + SOCIAL_AUTH_EVEONLINE_SCOPE = ['publicData'] diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template index a988d8286..919e8eafd 100644 --- a/examples/cherrypy_example/local_settings.py.template +++ b/examples/cherrypy_example/local_settings.py.template @@ -24,6 +24,7 @@ SOCIAL_SETTINGS = { 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 6747846a6..a66aaec68 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -136,6 +136,7 @@ 'social.backends.douban.DoubanOAuth2', 'social.backends.dropbox.DropboxOAuth', 'social.backends.dropbox.DropboxOAuth2', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.facebook.FacebookAppOAuth2', 'social.backends.facebook.FacebookOAuth2', diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 78e04d96e..caee14a5d 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -151,6 +151,7 @@ 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 101f1b8f5..9f830f803 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -35,6 +35,7 @@ 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py index 141b39323..fe9fb0180 100644 --- a/examples/flask_me_example/settings.py +++ b/examples/flask_me_example/settings.py @@ -40,6 +40,7 @@ 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index e96708c6d..5b2b8e296 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -29,6 +29,7 @@ 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index 978a2c7ea..3d4d0dae3 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -28,6 +28,7 @@ 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 9e8456ac4..354a513f9 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -41,6 +41,7 @@ 'social.backends.dailymotion.DailymotionOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', 'social.backends.fitbit.FitbitOAuth', 'social.backends.flickr.FlickrOAuth', diff --git a/social/backends/eveonline.py b/social/backends/eveonline.py new file mode 100644 index 000000000..c62439f5d --- /dev/null +++ b/social/backends/eveonline.py @@ -0,0 +1,39 @@ +""" +EVE Online Single Sign-On (SSO) OAuth2 backend +Documentation at https://developers.eveonline.com/resource/single-sign-on +""" +from requests import HTTPError + +from social.exceptions import AuthFailed +from social.backends.oauth import BaseOAuth2 + + +class EVEOnlineOAuth2(BaseOAuth2): + """EVE Online OAuth authentication backend""" + name = 'eveonline' + AUTHORIZATION_URL = 'https://login.eveonline.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://login.eveonline.com/oauth/token' + ID_KEY = "CharacterID" + ACCESS_TOKEN_METHOD = 'POST' + EXTRA_DATA = [ + ('CharacterID', 'id'), + ('refresh_token', 'refresh_token', True), + ('expires_in', 'expires'), + ] + + def get_user_details(self, response): + """Return user details from EVE Online account""" + user_data = self.user_data(response['access_token']) + print user_data + fullname, first_name, last_name = self.get_user_names(user_data['CharacterName']) + return {'username': fullname, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Get Character data from EVE server""" + return self.get_json( + 'https://login.eveonline.com/oauth/verify', + headers={'Authorization': "Bearer {0}".format(access_token)} + ) From 6dcb3dd44b4bfa2559002a95295ab254e3c0f110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 7 Mar 2015 22:24:57 -0200 Subject: [PATCH 491/890] PEP8, quotes and extra_data --- social/backends/eveonline.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/social/backends/eveonline.py b/social/backends/eveonline.py index c62439f5d..1a22ed650 100644 --- a/social/backends/eveonline.py +++ b/social/backends/eveonline.py @@ -2,9 +2,6 @@ EVE Online Single Sign-On (SSO) OAuth2 backend Documentation at https://developers.eveonline.com/resource/single-sign-on """ -from requests import HTTPError - -from social.exceptions import AuthFailed from social.backends.oauth import BaseOAuth2 @@ -13,27 +10,32 @@ class EVEOnlineOAuth2(BaseOAuth2): name = 'eveonline' AUTHORIZATION_URL = 'https://login.eveonline.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://login.eveonline.com/oauth/token' - ID_KEY = "CharacterID" + ID_KEY = 'CharacterID' ACCESS_TOKEN_METHOD = 'POST' EXTRA_DATA = [ ('CharacterID', 'id'), + ('ExpiresOn', 'expires'), + ('CharacterOwnerHash', 'owner_hash', True), ('refresh_token', 'refresh_token', True), - ('expires_in', 'expires'), ] def get_user_details(self, response): """Return user details from EVE Online account""" user_data = self.user_data(response['access_token']) - print user_data - fullname, first_name, last_name = self.get_user_names(user_data['CharacterName']) - return {'username': fullname, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} + fullname, first_name, last_name = self.get_user_names( + user_data['CharacterName'] + ) + return { + 'email': '', + 'username': fullname, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name + } def user_data(self, access_token, *args, **kwargs): """Get Character data from EVE server""" return self.get_json( 'https://login.eveonline.com/oauth/verify', - headers={'Authorization': "Bearer {0}".format(access_token)} + headers={'Authorization': 'Bearer {0}'.format(access_token)} ) From 829806fd36e45e6e0ccf8fcd364f7cac91da5b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Tue, 10 Mar 2015 20:53:02 +0100 Subject: [PATCH 492/890] Add wunderlist oauth2 backend --- docs/backends/index.rst | 1 + docs/backends/wunderlist.rst | 13 +++++++++ docs/intro.rst | 2 ++ .../local_settings.py.template | 1 + examples/django_example/example/settings.py | 1 + .../django_me_example/example/settings.py | 1 + examples/flask_example/settings.py | 1 + examples/flask_me_example/settings.py | 1 + examples/pyramid_example/example/settings.py | 1 + examples/tornado_example/settings.py | 1 + examples/webpy_example/app.py | 1 + social/backends/wunderlist.py | 29 +++++++++++++++++++ social/tests/backends/test_wunderlist.py | 26 +++++++++++++++++ 13 files changed, 79 insertions(+) create mode 100644 docs/backends/wunderlist.rst create mode 100644 social/backends/wunderlist.py create mode 100644 social/tests/backends/test_wunderlist.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 6af6473f5..0b4b3b4d6 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -121,6 +121,7 @@ Social backends vimeo vk weibo + wunderlist xing yahoo yammer diff --git a/docs/backends/wunderlist.rst b/docs/backends/wunderlist.rst new file mode 100644 index 000000000..218686d44 --- /dev/null +++ b/docs/backends/wunderlist.rst @@ -0,0 +1,13 @@ +Wunderlist +========== + +Wunderlist uses OAuth v2 for Authentication. + +- Register a new application at `Wunderlist Developer Portal`_, and + +- fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_WUNDERLIST_KEY = '' + SOCIAL_AUTH_WUNDERLIST_SECRET = '' + +.. _Wunderlist Developer Portal: https://developer.wunderlist.com/applications diff --git a/docs/intro.rst b/docs/intro.rst index 8e1282615..712f6db9f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -82,6 +82,7 @@ or extend current one): * Vimeo_ OAuth1 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 + * Wunderlist_ OAuth2 * Xing_ OAuth1 * Yahoo_ OpenId and OAuth1 * Yammer_ OAuth2 @@ -153,6 +154,7 @@ section. .. _Twitter: http://twitter.com .. _VK.com: http://vk.com .. _Weibo: http://weibo.com +.. _Wunderlist: http://wunderlist.com .. _Xing: https://www.xing.com .. _Yahoo: http://yahoo.com .. _Yammer: https://www.yammer.com diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template index 919e8eafd..e9fe9c0ef 100644 --- a/examples/cherrypy_example/local_settings.py.template +++ b/examples/cherrypy_example/local_settings.py.template @@ -38,6 +38,7 @@ SOCIAL_SETTINGS = { 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', ), 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': '', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': '' diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index a66aaec68..fff53dc9c 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -189,6 +189,7 @@ 'social.backends.twitter.TwitterOAuth', 'social.backends.vk.VKOAuth2', 'social.backends.weibo.WeiboOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', 'social.backends.xing.XingOAuth', 'social.backends.yahoo.YahooOAuth', 'social.backends.yahoo.YahooOpenId', diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index caee14a5d..9c2a15740 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -180,6 +180,7 @@ 'social.backends.amazon.AmazonOAuth2', 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', + 'social.backends.wunderlist.WunderlistOAuth2', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 9f830f803..8e0df5ef0 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -51,4 +51,5 @@ 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', ) diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py index fe9fb0180..26d9c5c41 100644 --- a/examples/flask_me_example/settings.py +++ b/examples/flask_me_example/settings.py @@ -57,4 +57,5 @@ 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', ) diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index 5b2b8e296..224917f6c 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -45,6 +45,7 @@ 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', ) } diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index 3d4d0dae3..e7e474215 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -44,6 +44,7 @@ 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', ) from local_settings import * diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 354a513f9..47d76c495 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -56,6 +56,7 @@ 'social.backends.yandex.YandexOAuth2', 'social.backends.podio.PodioOAuth2', 'social.backends.mineid.MineIDOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', ) web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/' diff --git a/social/backends/wunderlist.py b/social/backends/wunderlist.py new file mode 100644 index 000000000..9e3962831 --- /dev/null +++ b/social/backends/wunderlist.py @@ -0,0 +1,29 @@ +from social.backends.oauth import BaseOAuth2 + + +class WunderlistOAuth2(BaseOAuth2): + """Wunderlist OAuth2 authentication backend""" + name = 'wunderlist' + AUTHORIZATION_URL = 'https://www.wunderlist.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://www.wunderlist.com/oauth/access_token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + + def get_user_details(self, response): + """Return user details from Wunderlist account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) + return {'username': str(response.get('id')), + 'email': response.get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + headers = { + 'X-Access-Token': access_token, + 'X-Client-ID': self.setting('KEY')} + return self.get_json( + 'https://a.wunderlist.com/api/v1/user', headers=headers) diff --git a/social/tests/backends/test_wunderlist.py b/social/tests/backends/test_wunderlist.py new file mode 100644 index 000000000..0f16cfd0e --- /dev/null +++ b/social/tests/backends/test_wunderlist.py @@ -0,0 +1,26 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class WunderlistOAuth2Test(OAuth2Test): + backend_path = 'social.backends.wunderlist.WunderlistOAuth2' + user_data_url = 'https://a.wunderlist.com/api/v1/user' + expected_username = '12345' + access_token_body = json.dumps({ + 'access_token': 'foobar-token', + 'token_type': 'foobar'}) + user_data_body = json.dumps({ + 'created_at': '2015-01-21T00:56:51.442Z', + 'email': 'foo@bar.com', + 'id': 12345, + 'name': 'foobar', + 'revision': 1, + 'type': 'user', + 'updated_at': '2015-01-21T00:56:51.442Z'}) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 696d5b2d24b86dbd0d03dea46896a23d6532df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20E=C3=9Fer?= Date: Wed, 11 Mar 2015 18:09:48 +0100 Subject: [PATCH 493/890] Update index.html I fund a typo... --- site/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/index.html b/site/index.html index d1e80d2cb..230537bb5 100644 --- a/site/index.html +++ b/site/index.html @@ -83,7 +83,7 @@

          ORMs

          Development and Contact

          The code is available on Github, report any - issue if you fund any. Pull requests are + issue if you find any. Pull requests are always welcome. There's a mailing list and IRC channel #python-social-auth on Freenode network.

          From d042f21c87d299703a31e4dd0b1db3b9fde46a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Thu, 12 Mar 2015 08:36:36 +0100 Subject: [PATCH 494/890] Add wunderlist backend to the list I forgot update this file :) --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 11ee81cce..72b61c1f8 100644 --- a/README.rst +++ b/README.rst @@ -117,6 +117,7 @@ or current ones extended): * Twitter_ OAuth1 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 + * Wunderlist_ OAuth2 * Xing_ OAuth1 * Yahoo_ OpenId and OAuth2 * Yammer_ OAuth2 @@ -273,6 +274,7 @@ check `django-social-auth LICENSE`_ for details: .. _Twitter: http://twitter.com .. _VK.com: http://vk.com .. _Weibo: https://weibo.com +.. _Wunderlist: https://wunderlist.com .. _Xing: https://www.xing.com .. _Yahoo: http://yahoo.com .. _Yammer: https://www.yammer.com From bf6272f7f36f469c56bc97e45e9d0bb444ab95f0 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 12 Mar 2015 18:07:16 +0000 Subject: [PATCH 495/890] Increase min request-oauthlib version to 0.3.1 Fixes #545 --- requirements-python3.txt | 2 +- requirements.txt | 2 +- setup.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-python3.txt b/requirements-python3.txt index 22c68d3da..5f9ad376d 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -1,6 +1,6 @@ python3-openid>=3.0.1 requests>=1.1.0 oauthlib>=0.3.8 -requests-oauthlib>=0.3.0,<0.3.2 +requests-oauthlib>=0.3.1,<0.3.2 six>=1.2.0 PyJWT==0.4.1 diff --git a/requirements.txt b/requirements.txt index b0b0b9564..589bba437 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ python-openid>=2.2 requests>=1.1.0 oauthlib>=0.3.8 -requests-oauthlib>=0.3.0 +requests-oauthlib>=0.3.1 six>=1.2.0 PyJWT==0.4.1 diff --git a/setup.py b/setup.py index c6b468406..d1c4ddb00 100644 --- a/setup.py +++ b/setup.py @@ -49,9 +49,9 @@ def get_packages(): requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT==0.4.1'] if PY3: requires += ['python3-openid>=3.0.1', - 'requests-oauthlib>=0.3.0,<0.3.2'] + 'requests-oauthlib>=0.3.1,<0.3.2'] else: - requires += ['python-openid>=2.2', 'requests-oauthlib>=0.3.0'] + requires += ['python-openid>=2.2', 'requests-oauthlib>=0.3.1'] setup(name='python-social-auth', From f9597b9d0796dc555517fcd4d6cf67d9a7f76e9c Mon Sep 17 00:00:00 2001 From: Matt Howland Date: Thu, 12 Mar 2015 13:38:30 -0700 Subject: [PATCH 496/890] Create vend.py --- social/backends/vend.py | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 social/backends/vend.py diff --git a/social/backends/vend.py b/social/backends/vend.py new file mode 100644 index 000000000..013ca334a --- /dev/null +++ b/social/backends/vend.py @@ -0,0 +1,84 @@ +""" +Vend OAuth2 backend: + +""" +from requests import HTTPError +from social.backends.oauth import BaseOAuth2 +from social.exceptions import AuthCanceled, AuthUnknownError + + +class VendOAuth2(BaseOAuth2): + name = 'vend' + AUTHORIZATION_URL = 'https://secure.vendhq.com/connect' + ACCESS_TOKEN_URL = '' + SCOPE_SEPARATOR = ' ' + REDIRECT_STATE = False + REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' + EXTRA_DATA = [ + ('refresh_token', 'refresh_token'), + ('domain_prefix','domain_prefix') + ] + def get_user_id(self, details, response): + return None + def get_user_details(self, response): + return {} + + def user_data(self, access_token, *args, **kwargs): + + return None + + + def access_token_url(self): + return self.ACCESS_TOKEN_URL + + + + + def process_error(self, data): + error = data.get('error') + if error: + if error == 'access_denied': + raise AuthCanceled(self) + else: + raise AuthUnknownError(self, 'Vend error was {0}'.format( + error + )) + return super(VendOAuth2, self).process_error(data) + + def auth_complete_params(self, state=None): + client_id, client_secret = self.get_key_and_secret() + return { + 'code': self.data.get('code', '').encode('ascii', 'ignore'), # server response code + 'client_id': client_id, + 'client_secret': client_secret, + 'grant_type': 'authorization_code', # request auth code + 'redirect_uri': self.get_redirect_uri(state) + } + + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + + #Handle dynamic login access_token_url + self.ACCESS_TOKEN_URL = 'https://{0}.vendhq.com/api/1.0/token'.format(self.data["domain_prefix"]) + + self.process_error(self.data) + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + params=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method='POST', + + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + + raise + except KeyError: + raise AuthUnknownError(self) + self.process_error(response) + + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) From b51629fcc54ad755f1b17f27712aacc860f34129 Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Thu, 12 Mar 2015 20:47:26 -0400 Subject: [PATCH 497/890] update for django 1.9 --- social/apps/django_app/__init__.py | 7 ------- social/apps/django_app/default/config.py | 7 +++++++ social/strategies/django_strategy.py | 3 --- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/social/apps/django_app/__init__.py b/social/apps/django_app/__init__.py index 5225a4f5c..6ed7e2cb3 100644 --- a/social/apps/django_app/__init__.py +++ b/social/apps/django_app/__init__.py @@ -10,10 +10,3 @@ SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' """ -from social.strategies.utils import set_current_strategy_getter -from social.apps.django_app.utils import load_strategy - - -# Set strategy loader method to workaround current strategy getter needed on -# get_user() method on authentication backends when working with Django -set_current_strategy_getter(load_strategy) diff --git a/social/apps/django_app/default/config.py b/social/apps/django_app/default/config.py index 977cd3c6e..9658c577f 100644 --- a/social/apps/django_app/default/config.py +++ b/social/apps/django_app/default/config.py @@ -4,3 +4,10 @@ class PythonSocialAuthConfig(AppConfig): name = 'social.apps.django_app.default' verbose_name = 'Python Social Auth' + + def ready(self): + from social.strategies.utils import set_current_strategy_getter + from social.apps.django_app.utils import load_strategy + set_current_strategy_getter(load_strategy) + + diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index d0c439094..0f48368cd 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -5,7 +5,6 @@ from django.contrib.auth import authenticate from django.shortcuts import redirect from django.template import TemplateDoesNotExist, RequestContext, loader -from django.utils.datastructures import MergeDict from django.utils.translation import get_language from social.strategies.base import BaseStrategy, BaseTemplateStrategy @@ -106,8 +105,6 @@ def to_session_value(self, val): 'pk': val.pk, 'ctype': ContentType.objects.get_for_model(val).pk } - if isinstance(val, MergeDict): - val = dict(val) return val def from_session_value(self, val): From 183a57a33c1111bb9b19bfaac884cf82ae542073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 17 Mar 2015 19:47:34 -0300 Subject: [PATCH 498/890] Ensure to flush the db session (needed for Pyramid + sqlalchemy). Refs #390 --- social/storage/sqlalchemy_orm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index e7182cee8..edffb27fe 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -41,6 +41,7 @@ def _save_instance(cls, instance): cls._session().add(instance) if cls.COMMIT_SESSION: cls._session().commit() + cls._session().flush() return instance def save(self): From 58915b1b32378c51ffc3f4e32e3afffb580da41b Mon Sep 17 00:00:00 2001 From: Jerome Lefeuvre Date: Thu, 19 Mar 2015 13:41:14 -0400 Subject: [PATCH 499/890] Add `python_chameleon` to setup --- examples/pyramid_example/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pyramid_example/setup.py b/examples/pyramid_example/setup.py index c86070c48..0b4318e67 100644 --- a/examples/pyramid_example/setup.py +++ b/examples/pyramid_example/setup.py @@ -16,6 +16,7 @@ 'pyramid_debugtoolbar', 'zope.sqlalchemy', 'waitress', + 'pyramid_chameleon', ] setup(name='example', From 1208387d7e6e24172a175f28299380916b275f91 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 19 Mar 2015 19:01:30 +0000 Subject: [PATCH 500/890] Start pipeline with default details arg Fixes #555 --- social/backends/base.py | 1 + social/pipeline/social_auth.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/social/backends/base.py b/social/backends/base.py index 4b5c666e3..bceebb617 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -103,6 +103,7 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): out.setdefault('strategy', self.strategy) out.setdefault('backend', out.pop(self.name, None) or self) out.setdefault('request', self.strategy.request_data()) + out.setdefault('details', {}) for idx, name in enumerate(pipeline): out['pipeline_index'] = pipeline_index + idx diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 90aae6438..b790870af 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -2,8 +2,8 @@ AuthForbidden -def social_details(backend, response, *args, **kwargs): - return {'details': backend.get_user_details(response)} +def social_details(backend, details, response, *args, **kwargs): + return {'details': dict(backend.get_user_details(response), **details)} def social_uid(backend, details, response, *args, **kwargs): From a52ee69cc641018bbf32c6fd599a6c30036b3086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 19 Mar 2015 16:28:11 -0300 Subject: [PATCH 501/890] Flush sqlalchemy session to get the object ids. Refs #390 --- examples/pyramid_example/example/auth.py | 4 ++-- social/apps/pyramid_app/models.py | 2 -- social/storage/sqlalchemy_orm.py | 23 +++++++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/pyramid_example/example/auth.py b/examples/pyramid_example/example/auth.py index 98526f2a5..23926fe5e 100644 --- a/examples/pyramid_example/example/auth.py +++ b/examples/pyramid_example/example/auth.py @@ -5,8 +5,8 @@ from example.models import DBSession, User -def login_user(strategy, user, user_social_auth): - strategy.strategy.session_set('user_id', user.id) +def login_user(backend, user, user_social_auth): + backend.strategy.session_set('user_id', user.id) def login_required(request): diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py index 912cf0062..c38bf00b0 100644 --- a/social/apps/pyramid_app/models.py +++ b/social/apps/pyramid_app/models.py @@ -24,8 +24,6 @@ def init_social(config, Base, session): app_session = session class _AppSession(object): - COMMIT_SESSION = False - @classmethod def _session(cls): return app_session diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index edffb27fe..6c105edd4 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -3,6 +3,8 @@ import six import json +import transaction + from sqlalchemy import Column, Integer, String from sqlalchemy.exc import IntegrityError from sqlalchemy.types import PickleType, Text @@ -22,8 +24,6 @@ def __init__(self, *args, **kwargs): class SQLAlchemyMixin(object): - COMMIT_SESSION = True - @classmethod def _session(cls): raise NotImplementedError('Implement in subclass') @@ -39,11 +39,18 @@ def _new_instance(cls, model, *args, **kwargs): @classmethod def _save_instance(cls, instance): cls._session().add(instance) - if cls.COMMIT_SESSION: - cls._session().commit() - cls._session().flush() + cls._flush() return instance + @classmethod + def _flush(cls): + try: + cls._session().flush() + except AssertionError: + with transaction.manager as manager: + print "COMMIT 5" + manager.commit() + def save(self): self._save_instance(self) @@ -84,11 +91,7 @@ def allowed_to_disconnect(cls, user, backend_name, association_id=None): @classmethod def disconnect(cls, entry): cls._session().delete(entry) - try: - cls._session().commit() - except AssertionError: - import transaction - transaction.commit() + cls._flush() @classmethod def user_query(cls): From 70d06c4b9bef7eca8b003503f8ab860876e860d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 19 Mar 2015 17:53:13 -0300 Subject: [PATCH 502/890] Remove debug print --- social/storage/sqlalchemy_orm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 6c105edd4..978d2b988 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -48,7 +48,6 @@ def _flush(cls): cls._session().flush() except AssertionError: with transaction.manager as manager: - print "COMMIT 5" manager.commit() def save(self): From 3718ac7d63132105caf9585c6b75e299d9f6ccb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 19 Mar 2015 19:48:35 -0400 Subject: [PATCH 503/890] Require PyJWT>=1.0.0,<2.0.0 --- requirements-python3.txt | 2 +- requirements.txt | 2 +- setup.py | 3 ++- social/tests/requirements-python3.txt | 2 +- social/tests/requirements.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements-python3.txt b/requirements-python3.txt index 5f9ad376d..586b499a5 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -3,4 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.1,<0.3.2 six>=1.2.0 -PyJWT==0.4.1 +PyJWT>=1.0.0,<2.0.0 diff --git a/requirements.txt b/requirements.txt index 589bba437..c98195be9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.1 six>=1.2.0 -PyJWT==0.4.1 +PyJWT>=1.0.0,<2.0.0 diff --git a/setup.py b/setup.py index d1c4ddb00..3328fce42 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,8 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT==0.4.1'] +requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', + 'PyJWT>=1.0.0,<2.0.0'] if PY3: requires += ['python3-openid>=3.0.1', 'requests-oauthlib>=0.3.1,<0.3.2'] diff --git a/social/tests/requirements-python3.txt b/social/tests/requirements-python3.txt index ea2d9893e..23fc412f5 100644 --- a/social/tests/requirements-python3.txt +++ b/social/tests/requirements-python3.txt @@ -3,5 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -PyJWT==0.4.1 +PyJWT>=1.0.0,<2.0.0 unittest2py3k==0.5.1 diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index c8a69d1d5..1281a8ffe 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,5 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 requests>=1.1.0 -PyJWT==0.4.1 +PyJWT>=1.0.0,<2.0.0 unittest2==0.5.1 From d25b5fd294939b49d27db48e7b1ce8204bcea561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 19 Mar 2015 19:48:49 -0400 Subject: [PATCH 504/890] Specify algorithm for encoding and decoding --- social/backends/exacttarget.py | 2 +- social/backends/open_id.py | 3 ++- social/tests/backends/open_id.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/social/backends/exacttarget.py b/social/backends/exacttarget.py index 05892971a..ca49ac9b5 100644 --- a/social/backends/exacttarget.py +++ b/social/backends/exacttarget.py @@ -61,7 +61,7 @@ def process_error(self, data): def do_auth(self, token, *args, **kwargs): dummy, secret = self.get_key_and_secret() try: # Decode the token, using the Application Signature from settings - decoded = jwt.decode(token, secret) + decoded = jwt.decode(token, secret, algorithms=['HS256']) except jwt.DecodeError: # Wrong signature, fail authentication raise AuthCanceled(self) kwargs.update({'response': {'token': decoded}, 'backend': self}) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 0c7b23ea8..63babb95d 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -330,7 +330,8 @@ def validate_and_return_id_token(self, id_token): # Decode the JWT and raise an error if the secret is invalid or # the response has expired. id_token = jwt_decode(id_token, decryption_key, audience=client_id, - issuer=self.ID_TOKEN_ISSUER) + issuer=self.ID_TOKEN_ISSUER, + algorithms=['HS256']) except InvalidTokenError as err: raise AuthTokenError(self, err) diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index e0d6e01af..22e6d45e3 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -193,7 +193,8 @@ def prepare_access_token_body(self, client_key=None, client_secret=None, client_key, timegm(expiration_datetime.utctimetuple()), timegm(issue_datetime.utctimetuple()), nonce, issuer) - body['id_token'] = jwt.encode(id_token, client_secret).decode('utf-8') + body['id_token'] = jwt.encode(id_token, client_secret, + algorithm='HS256').decode('utf-8') return json.dumps(body) def authtoken_raised(self, expected_message, **access_token_kwargs): From 52f6f1450299aee86730bb430228bf2ed6bb9774 Mon Sep 17 00:00:00 2001 From: Andrei Petre Date: Sat, 21 Mar 2015 15:15:53 +0000 Subject: [PATCH 505/890] Add missing migration for Django app --- .../migrations/0002_add_related_name.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 social/apps/django_app/default/migrations/0002_add_related_name.py diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py new file mode 100644 index 000000000..d33d47aae --- /dev/null +++ b/social/apps/django_app/default/migrations/0002_add_related_name.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('default', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='usersocialauth', + name='user', + field=models.ForeignKey(related_name='social_auth', to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + ] From 4d17fcc5b4ac87350b9e87587693d51971c8f800 Mon Sep 17 00:00:00 2001 From: Jerome Lefeuvre Date: Mon, 23 Mar 2015 11:04:44 -0400 Subject: [PATCH 506/890] Add rednose for colored output log --- social/tests/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index c8a69d1d5..eb7022dc9 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -2,6 +2,7 @@ httpretty==0.6.5 coverage>=3.6 mock==1.0.1 nose>=1.2.1 +rednose>=0.4.1 requests>=1.1.0 PyJWT==0.4.1 unittest2==0.5.1 From d65e6ceac03b819847b93b76d016203bd22644ac Mon Sep 17 00:00:00 2001 From: Jerome Lefeuvre Date: Mon, 23 Mar 2015 11:05:15 -0400 Subject: [PATCH 507/890] Add setup.cfg to configure flake8 and nosetests --- setup.cfg | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..1c117ce01 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[flake8] +max-line-length = 119 +# Ignore some well known paths +exclude = .venv,.tox,dist,doc,build,*.egg,db/env.py,db/versions/*.py + +[nosetests] +verbosity=2 +with-coverage=1 +cover-erase=1 +cover-package=social +rednose=1 \ No newline at end of file From 67dc33f8a14eb5ea8ceaf45601b86a46d7bf33a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 25 Mar 2015 00:43:32 -0300 Subject: [PATCH 508/890] Add rednose to python3 requirements too --- setup.cfg | 2 +- social/tests/requirements-python3.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1c117ce01..365848c3c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,4 @@ verbosity=2 with-coverage=1 cover-erase=1 cover-package=social -rednose=1 \ No newline at end of file +rednose=1 diff --git a/social/tests/requirements-python3.txt b/social/tests/requirements-python3.txt index ea2d9893e..995173b4d 100644 --- a/social/tests/requirements-python3.txt +++ b/social/tests/requirements-python3.txt @@ -2,6 +2,7 @@ httpretty==0.6.5 coverage>=3.6 mock==1.0.1 nose>=1.2.1 +rednose>=0.4.1 requests>=1.1.0 PyJWT==0.4.1 unittest2py3k==0.5.1 From 7267581f0acaef5830dfac18bfe075f2df82b224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 25 Mar 2015 01:44:53 -0300 Subject: [PATCH 509/890] Fix backend, add quick docs. Refs #549 --- docs/backends/index.rst | 1 + docs/backends/vend.rst | 24 ++++++ examples/django_example/example/settings.py | 1 + social/backends/vend.py | 87 +++++---------------- 4 files changed, 47 insertions(+), 66 deletions(-) create mode 100644 docs/backends/vend.rst diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 0b4b3b4d6..0859a74d2 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -118,6 +118,7 @@ Social backends twilio twitch twitter + vend vimeo vk weibo diff --git a/docs/backends/vend.rst b/docs/backends/vend.rst new file mode 100644 index 000000000..880698e59 --- /dev/null +++ b/docs/backends/vend.rst @@ -0,0 +1,24 @@ +Vend +==== + +Vend supports OAuth 2. + +- Register a new application at `Vend Developers Portal`_ + +- Add the Vend OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.vend.VendOAuth2', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_VEND_OAUTH2_KEY = '' + SOCIAL_AUTH_VEND_OAUTH2_SECRET = '' + +More details on their docs_. + +.. _Vend Developers Portal: https://developers.vendhq.com/developer/applications +.. _docs: https://developers.vendhq.com/documentation diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index fff53dc9c..58ab9c720 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -198,6 +198,7 @@ 'social.backends.vimeo.VimeoOAuth1', 'social.backends.lastfm.LastFmAuth', 'social.backends.moves.MovesOAuth2', + 'social.backends.vend.VendOAuth2', 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', diff --git a/social/backends/vend.py b/social/backends/vend.py index 013ca334a..5c3927af4 100644 --- a/social/backends/vend.py +++ b/social/backends/vend.py @@ -1,84 +1,39 @@ """ Vend OAuth2 backend: - """ -from requests import HTTPError from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthCanceled, AuthUnknownError class VendOAuth2(BaseOAuth2): name = 'vend' AUTHORIZATION_URL = 'https://secure.vendhq.com/connect' - ACCESS_TOKEN_URL = '' - SCOPE_SEPARATOR = ' ' + ACCESS_TOKEN_URL = 'https://{0}.vendhq.com/api/1.0/token' + ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False - REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' EXTRA_DATA = [ - ('refresh_token', 'refresh_token'), - ('domain_prefix','domain_prefix') + ('refresh_token', 'refresh_token'), + ('domain_prefix', 'domain_prefix') ] - def get_user_id(self, details, response): - return None - def get_user_details(self, response): - return {} - - def user_data(self, access_token, *args, **kwargs): - - return None - def access_token_url(self): - return self.ACCESS_TOKEN_URL - - - + return self.ACCESS_TOKEN_URL.format(self.data['domain_prefix']) - def process_error(self, data): - error = data.get('error') - if error: - if error == 'access_denied': - raise AuthCanceled(self) - else: - raise AuthUnknownError(self, 'Vend error was {0}'.format( - error - )) - return super(VendOAuth2, self).process_error(data) - - def auth_complete_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() + def get_user_details(self, response): + email = response['email'] + username = response.get('username') or email.split('@', 1)[0] return { - 'code': self.data.get('code', '').encode('ascii', 'ignore'), # server response code - 'client_id': client_id, - 'client_secret': client_secret, - 'grant_type': 'authorization_code', # request auth code - 'redirect_uri': self.get_redirect_uri(state) + 'username': username, + 'email': email, + 'fullname': '', + 'first_name': '', + 'last_name': '' } - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - - #Handle dynamic login access_token_url - self.ACCESS_TOKEN_URL = 'https://{0}.vendhq.com/api/1.0/token'.format(self.data["domain_prefix"]) - - self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - params=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method='POST', - - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - - raise - except KeyError: - raise AuthUnknownError(self) - self.process_error(response) - - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + prefix = kwargs['response']['domain_prefix'] + url = 'https://{0}.vendhq.com/api/users'.format(prefix) + data = self.get_json(url, headers={ + 'Authorization': 'Bearer {0}'.format(access_token) + }) + return data['users'][0] if data.get('users') else {} From 451f8b381f98f70066bf9a37b2b14fa4681c4e34 Mon Sep 17 00:00:00 2001 From: Jun Wang Date: Wed, 25 Mar 2015 07:33:44 -0400 Subject: [PATCH 510/890] set redirect_state to false for live oauth2 --- social/backends/live.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/live.py b/social/backends/live.py index 35af9203b..a7dda92e8 100644 --- a/social/backends/live.py +++ b/social/backends/live.py @@ -23,6 +23,7 @@ class LiveOAuth2(BaseOAuth2): ('last_name', 'last_name'), ('token_type', 'token_type'), ] + REDIRECT_STATE = False def get_user_details(self, response): """Return user details from Live Connect account""" From 72d413abb4b5b8e713960f87587e01bdb74dbc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 29 Mar 2015 14:08:04 -0300 Subject: [PATCH 511/890] Store github login in extra data by default. Refs #567 --- social/backends/github.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/github.py b/social/backends/github.py index db58cc1fa..9ac091c5f 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -17,7 +17,8 @@ class GithubOAuth2(BaseOAuth2): SCOPE_SEPARATOR = ',' EXTRA_DATA = [ ('id', 'id'), - ('expires', 'expires') + ('expires', 'expires'), + ('login', 'login') ] def get_user_details(self, response): From bcac402f7f2bf665fbd1550120b344a7bfd1114d Mon Sep 17 00:00:00 2001 From: "Buddy Lindsey, Jr." Date: Sun, 29 Mar 2015 14:24:14 -0500 Subject: [PATCH 512/890] Add revoke token ability to strava Currently you can't disconnect strava from the site. This sets the deauthorize url and sets approriate data for revoking token on strava site. --- social/backends/strava.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/social/backends/strava.py b/social/backends/strava.py index b0a2a81f7..2eb6fcdf3 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -15,6 +15,7 @@ class StravaOAuth(BaseOAuth2): # http://example.com/complete/strava?redirect_state=xxx?code=xxx&state=xxx # Check issue #259 for details. REDIRECT_STATE = False + REVOKE_TOKEN_URL = 'https://www.strava.com/oauth/deauthorize' def get_user_id(self, details, response): return response['athlete']['id'] @@ -38,3 +39,8 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://www.strava.com/api/v3/athlete', params={'access_token': access_token}) + + def revoke_token_params(self, token, uid): + params = super(StravaOAuth, self).revoke_token_params(token, uid) + params['access_token'] = token + return params From 3d182459b8556178584a184450cdd278e71cea97 Mon Sep 17 00:00:00 2001 From: Krzysztof Hoffmann Date: Mon, 30 Mar 2015 16:48:53 +0200 Subject: [PATCH 513/890] Added NaszaKlasa OAuth2 support --- README.rst | 2 ++ docs/backends/naszaklasa.rst | 26 ++++++++++++++ docs/intro.rst | 2 ++ social/backends/nk.py | 70 ++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 docs/backends/naszaklasa.rst create mode 100644 social/backends/nk.py diff --git a/README.rst b/README.rst index 72b61c1f8..5817f6733 100644 --- a/README.rst +++ b/README.rst @@ -90,6 +90,7 @@ or current ones extended): * Mendeley_ OAuth1 http://mendeley.com * Mixcloud_ OAuth2 * `Mozilla Persona`_ + * NaszaKlasa_ OAuth2 * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth @@ -259,6 +260,7 @@ check `django-social-auth LICENSE`_ for details: .. _MapMyFitness: http://www.mapmyfitness.com/ .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ +.. _NaszaKlasa: https://developers.nk.pl/ .. _Odnoklassniki: http://www.odnoklassniki.ru .. _Pocket: http://getpocket.com .. _Podio: https://podio.com diff --git a/docs/backends/naszaklasa.rst b/docs/backends/naszaklasa.rst new file mode 100644 index 000000000..01fe78e99 --- /dev/null +++ b/docs/backends/naszaklasa.rst @@ -0,0 +1,26 @@ +NationBuilder +============= + +`NaszaKlasa supports OAuth2`_ as their authentication mechanism. Follow these +steps in order to use it: + +- Register a new application at your `NK Developers`_ (define the `Callback + URL` to ``http://example.com/complete/nk/`` where ``example.com`` + is your domain). + +- Fill the ``Client ID`` and ``Client Secret`` values from the newly created + application:: + + SOCIAL_AUTH_NK_KEY = '' + SOCIAL_AUTH_NK_SECRET = '' + +- Enable the backend in ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.nk.NKOAuth2', + ... + ) + +.. _NaszaKlasa supports OAuth2: https://developers.nk.pl +.. _NK Developers: https://developers.nk.pl/developers/oauth2client/form \ No newline at end of file diff --git a/docs/intro.rst b/docs/intro.rst index 712f6db9f..b835961f4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -60,6 +60,7 @@ or extend current one): * MineID_ OAuth2 * Mixcloud_ OAuth2 * `Mozilla Persona`_ + * NaszaKlasa_ OAuth2 * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ * Podio_ OAuth2 @@ -139,6 +140,7 @@ section. .. _MineID: https://www.mineid.org .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ +.. _NaszaKlasa: https://developers.nk.pl/ .. _Odnoklassniki: http://www.odnoklassniki.ru .. _Podio: https://podio.com .. _Shopify: http://shopify.com diff --git a/social/backends/nk.py b/social/backends/nk.py new file mode 100644 index 000000000..0ad381322 --- /dev/null +++ b/social/backends/nk.py @@ -0,0 +1,70 @@ +import json +from urllib import urlencode, urlopen +from requests_oauthlib import OAuth1 + +from social.backends.oauth import BaseOAuth2 +import six + +class NKOAuth2(BaseOAuth2): + """NK OAuth authentication backend""" + name = 'nk' + AUTHORIZATION_URL = 'https://nk.pl/oauth2/login' + ACCESS_TOKEN_URL = 'https://nk.pl/oauth2/token' + SCOPE_SEPARATOR = ',' + ACCESS_TOKEN_METHOD = 'POST' + SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER' + EXTRA_DATA = [ + ('id', 'id'), + ] + + def get_user_details(self, response): + """Return user details from NK account""" + entry = response['entry'] + return {'username': entry.get('displayName'), + 'email': entry['emails'][0]['value'], + 'first_name': entry.get('displayName').split(" ")[0], + 'id':entry.get('id')} + + def auth_complete_params(self, state=None): + client_id, client_secret = self.get_key_and_secret() + return { + 'grant_type': 'authorization_code', # request auth code + 'code': self.data.get('code', ''), # server response code + 'client_id': client_id, + 'client_secret': client_secret, + 'redirect_uri': self.get_redirect_uri(state), + 'scope':self.get_scope_argument() + } + + def get_user_id(self, details, response): + """Return a unique ID for the current user, by default from server + response.""" + return details.get(self.ID_KEY) + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + url = 'http://opensocial.nk-net.pl/v09/social/rest/people/@me?' + urlencode({ + 'nk_token': access_token, + 'fields': 'name,surname,avatar,localization,age,gender,emails,birthdate' + }) + return self.get_json( + url, + auth=self.oauth_auth(access_token) + ) + + def oauth_auth(self, token=None, oauth_verifier=None, + signature_type=SIGNATURE_TYPE_AUTH_HEADER): + key, secret = self.get_key_and_secret() + oauth_verifier = oauth_verifier or self.data.get('oauth_verifier') + token = token or {} + # decoding='utf-8' produces errors with python-requests on Python3 + # since the final URL will be of type bytes + decoding = None if six.PY3 else 'utf-8' + state = self.get_or_create_state() + return OAuth1(key, secret, + resource_owner_key=None, + resource_owner_secret=None, + callback_uri=self.get_redirect_uri(state), + verifier=oauth_verifier, + signature_type=signature_type, + decoding=decoding) \ No newline at end of file From 05a4c08352b7c72a08a7df0c709e6172f4ce33c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 31 Mar 2015 12:12:37 -0300 Subject: [PATCH 514/890] PEP8. Refs #570 --- social/backends/nk.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/social/backends/nk.py b/social/backends/nk.py index 0ad381322..1446ad179 100644 --- a/social/backends/nk.py +++ b/social/backends/nk.py @@ -1,9 +1,11 @@ -import json -from urllib import urlencode, urlopen +from urllib import urlencode + +import six + from requests_oauthlib import OAuth1 from social.backends.oauth import BaseOAuth2 -import six + class NKOAuth2(BaseOAuth2): """NK OAuth authentication backend""" @@ -20,10 +22,12 @@ class NKOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from NK account""" entry = response['entry'] - return {'username': entry.get('displayName'), - 'email': entry['emails'][0]['value'], - 'first_name': entry.get('displayName').split(" ")[0], - 'id':entry.get('id')} + return { + 'username': entry.get('displayName'), + 'email': entry['emails'][0]['value'], + 'first_name': entry.get('displayName').split(' ')[0], + 'id': entry.get('id') + } def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() @@ -33,7 +37,7 @@ def auth_complete_params(self, state=None): 'client_id': client_id, 'client_secret': client_secret, 'redirect_uri': self.get_redirect_uri(state), - 'scope':self.get_scope_argument() + 'scope': self.get_scope_argument() } def get_user_id(self, details, response): @@ -67,4 +71,4 @@ def oauth_auth(self, token=None, oauth_verifier=None, callback_uri=self.get_redirect_uri(state), verifier=oauth_verifier, signature_type=signature_type, - decoding=decoding) \ No newline at end of file + decoding=decoding) From 43a594c3cdf968fca0cb6ff7ee11f8f7a7d14413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 31 Mar 2015 15:46:20 -0300 Subject: [PATCH 515/890] v0.2.3 --- Changelog | 129 +++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index fc0446cfc..17d3cb363 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,129 @@ +2015-03-31 HEAD (unreleased) +2015-03-31 v0.2.3 +================= + + * 2015-03-31 Matías Aguirre + PEP8. Refs #570 + + * 2015-03-30 Krzysztof Hoffmann + Added NaszaKlasa OAuth2 support + + * 2015-03-29 Buddy Lindsey, Jr. + Add revoke token ability to strava + + * 2015-03-29 Matías Aguirre + Store github login in extra data by default. Refs #567 + + * 2015-03-25 Jun Wang + set redirect_state to false for live oauth2 + + * 2015-03-25 Matías Aguirre + Fix backend, add quick docs. Refs #549 + + * 2015-03-25 Matías Aguirre + Add rednose to python3 requirements too + + * 2015-03-23 Jerome Lefeuvre + Add setup.cfg to configure flake8 and nosetests + + * 2015-03-23 Jerome Lefeuvre + Add rednose for colored output log + + * 2015-03-21 Andrei Petre + Add missing migration for Django app + + * 2015-03-19 José Padilla + Specify algorithm for encoding and decoding + + * 2015-03-19 José Padilla + Require PyJWT>=1.0.0,<2.0.0 + + * 2015-03-19 Matías Aguirre + Remove debug print + + * 2015-03-19 Matías Aguirre + Flush sqlalchemy session to get the object ids. Refs #390 + + * 2015-03-19 Johannes + Start pipeline with default details arg + + * 2015-03-19 Jerome Lefeuvre + Add `python_chameleon` to setup + + * 2015-03-17 Matías Aguirre + Ensure to flush the db session (needed for Pyramid + sqlalchemy). Refs #390 + + * 2015-03-12 Matt Howland + Create vend.py + + * 2015-03-12 Johannes + Increase min request-oauthlib version to 0.3.1 + + * 2015-03-12 Adam Bogdał + Add wunderlist backend to the list + + * 2015-03-11 Florian Eßer + Update index.html + + * 2015-03-10 Adam Bogdał + Add wunderlist oauth2 backend + + * 2015-03-07 Matías Aguirre + PEP8, quotes and extra_data + + * 2015-03-06 Florian Eßer + Add backend for EVE Online Single Sign-On (OAuth2) + https://developers.eveonline.com/resource/single-sign-on + + * 2015-03-05 Matías Aguirre + PEP8 and simplify code + + * 2015-03-05 Matías Aguirre + PEP8 + + * 2015-03-05 Rafael Muñoz Cárdenas + Add extra info on Google+ Sign-In doc + + * 2015-03-03 dobestan + update Kakao OAuth2 backend : update auth process- Fixes #538 + + * 2015-03-03 dobestan + Enable KakaoOAuth2 on example app + + * 2015-03-03 dobestan + Disable redirect_state in kakao backend. Fixes #538 + + * 2015-03-02 Tom Clancy + Update google.rst + + * 2015-03-02 Hassek + modified docs + + * 2015-02-25 Hassek + fixed refresh tokens for yahoo + + * 2015-02-25 Hassek + added OAuth2 support to yahoo. Also, removed OAuth1 since yahoo will not be + supporting it anymore + + * 2015-02-24 Matías Aguirre + Cleanup imports and hmac creation, fix python3 compatibility + + * 2015-02-24 zz + Fix Issue #532, get UID when use access_token ajax auth in weibo backends. + + * 2015-02-24 zz + Fix Issue #532, get UID when use access_token ajax auth in weibo backends. + + * 2015-02-23 Matías Aguirre + Fix zotero tests + 2015-02-23 v0.2.2 ================= + * 2015-02-23 Matías Aguirre + v0.2.2 + * 2015-02-23 Matías Aguirre PEP8/PyFlakes @@ -36,6 +159,9 @@ * 2015-02-13 Chris Martin Include username in Reddit extra_data + * 2015-02-12 Eugene Agafonov + [facebook-oauth2] Verifying Graph API Calls with appsecret_proof + * 2015-02-11 tell-k refs #512 fixed typo @@ -157,6 +283,9 @@ * 2014-11-27 James Potter Update django.rst + * 2014-11-26 Anna Warzecha + User ID is required to use any further requests + * 2014-11-26 Sasha Golubev Added backend for professionali.ru diff --git a/social/__init__.py b/social/__init__.py index 7df9523d1..6baaa950a 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 2) +version = (0, 2, 3) extra = '' __version__ = '.'.join(map(str, version)) + extra From 5e048614b60d09e3dbcc44750f29496f5e27ed3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2EYasoob=20Ullah=20Khalid=20=E2=98=BA?= Date: Wed, 1 Apr 2015 19:16:51 +0500 Subject: [PATCH 516/890] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d2e8892ad..55490c108 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2013, Matías Aguirre +Copyright (c) 2012-2015, Matías Aguirre All rights reserved. Redistribution and use in source and binary forms, with or without modification, From af16a6d2d795c1ef472193db60dad00f4b5327cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 2 Apr 2015 18:26:16 -0300 Subject: [PATCH 517/890] Add backward compatibility on django app initialization. Refs #550 --- social/apps/django_app/__init__.py | 9 +++++++++ social/apps/django_app/default/config.py | 5 +++-- social/apps/django_app/me/__init__.py | 2 ++ social/apps/django_app/me/config.py | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 social/apps/django_app/me/config.py diff --git a/social/apps/django_app/__init__.py b/social/apps/django_app/__init__.py index 6ed7e2cb3..217d3cea8 100644 --- a/social/apps/django_app/__init__.py +++ b/social/apps/django_app/__init__.py @@ -10,3 +10,12 @@ SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' """ +import django + + +if django.VERSION[0] == 1 and django.VERSION[1] < 7: + from social.strategies.utils import set_current_strategy_getter + from social.apps.django_app.utils import load_strategy + # Set strategy loader method to workaround current strategy getter needed on + # get_user() method on authentication backends when working with Django + set_current_strategy_getter(load_strategy) diff --git a/social/apps/django_app/default/config.py b/social/apps/django_app/default/config.py index 9658c577f..745e24d9c 100644 --- a/social/apps/django_app/default/config.py +++ b/social/apps/django_app/default/config.py @@ -8,6 +8,7 @@ class PythonSocialAuthConfig(AppConfig): def ready(self): from social.strategies.utils import set_current_strategy_getter from social.apps.django_app.utils import load_strategy + # Set strategy loader method to workaround current strategy getter + # needed on get_user() method on authentication backends when working + # with Django set_current_strategy_getter(load_strategy) - - diff --git a/social/apps/django_app/me/__init__.py b/social/apps/django_app/me/__init__.py index 0ba8d188e..9bc91e231 100644 --- a/social/apps/django_app/me/__init__.py +++ b/social/apps/django_app/me/__init__.py @@ -5,3 +5,5 @@ * Add 'social.apps.django_app.me' to INSTALLED_APPS * In urls.py include url('', include('social.apps.django_app.urls')) """ +default_app_config = \ + 'social.apps.django_app.me.config.PythonSocialAuthConfig' diff --git a/social/apps/django_app/me/config.py b/social/apps/django_app/me/config.py new file mode 100644 index 000000000..1ece281f4 --- /dev/null +++ b/social/apps/django_app/me/config.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig + + +class PythonSocialAuthConfig(AppConfig): + name = 'social.apps.django_app.me' + verbose_name = 'Python Social Auth' + + def ready(self): + from social.strategies.utils import set_current_strategy_getter + from social.apps.django_app.utils import load_strategy + # Set strategy loader method to workaround current strategy getter + # needed on get_user() method on authentication backends when working + # with Django + set_current_strategy_getter(load_strategy) From da84831f62ea0468a1eb112e316fa74de8e81e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 2 Apr 2015 23:55:49 -0300 Subject: [PATCH 518/890] Update django/mongoengine example (similar to default one). Refs #576 --- .../example/app/decorators.py | 16 + .../django_me_example/example/app/mail.py | 16 +- .../django_me_example/example/app/models.py | 7 - .../django_me_example/example/app/pipeline.py | 7 +- .../example/app/templatetags/__init__.py | 0 .../example/app/templatetags/backend_utils.py | 82 +++ .../django_me_example/example/app/views.py | 73 ++- .../django_me_example/example/settings.py | 12 +- .../example/templates/base.html | 14 - .../example/templates/done.html | 25 - .../example/templates/email.html | 11 - .../example/templates/email_signup.html | 19 - .../example/templates/home.html | 533 ++++++++++++++---- .../example/templates/username_signup.html | 19 - .../example/templates/validation_sent.html | 6 - examples/django_me_example/example/urls.py | 4 +- examples/django_me_example/example/wsgi.py | 2 +- examples/django_me_example/requirements.txt | 2 +- 18 files changed, 614 insertions(+), 234 deletions(-) create mode 100644 examples/django_me_example/example/app/decorators.py create mode 100644 examples/django_me_example/example/app/templatetags/__init__.py create mode 100644 examples/django_me_example/example/app/templatetags/backend_utils.py delete mode 100644 examples/django_me_example/example/templates/base.html delete mode 100644 examples/django_me_example/example/templates/done.html delete mode 100644 examples/django_me_example/example/templates/email.html delete mode 100644 examples/django_me_example/example/templates/email_signup.html delete mode 100644 examples/django_me_example/example/templates/username_signup.html delete mode 100644 examples/django_me_example/example/templates/validation_sent.html diff --git a/examples/django_me_example/example/app/decorators.py b/examples/django_me_example/example/app/decorators.py new file mode 100644 index 000000000..2ba85b130 --- /dev/null +++ b/examples/django_me_example/example/app/decorators.py @@ -0,0 +1,16 @@ +from functools import wraps + +from django.template import RequestContext +from django.shortcuts import render_to_response + + +def render_to(tpl): + def decorator(func): + @wraps(func) + def wrapper(request, *args, **kwargs): + out = func(request, *args, **kwargs) + if isinstance(out, dict): + out = render_to_response(tpl, out, RequestContext(request)) + return out + return wrapper + return decorator diff --git a/examples/django_me_example/example/app/mail.py b/examples/django_me_example/example/app/mail.py index 238251cec..4dd59b5a7 100644 --- a/examples/django_me_example/example/app/mail.py +++ b/examples/django_me_example/example/app/mail.py @@ -3,11 +3,11 @@ from django.core.urlresolvers import reverse -def send_validation(strategy, code): - url = reverse('social:complete', args=(strategy.backend_name,)) \ - + '?verification_code=' + code.code - send_mail('Validate your account', - 'Validate your account {0}'.format(url), - settings.EMAIL_FROM, - [code.email], - fail_silently=False) +def send_validation(strategy, backend, code): + url = '{0}?verification_code={1}'.format( + reverse('social:complete', args=(backend.name,)), + code.code + ) + url = strategy.request.build_absolute_uri(url) + send_mail('Validate your account', 'Validate your account {0}'.format(url), + settings.EMAIL_FROM, [code.email], fail_silently=False) diff --git a/examples/django_me_example/example/app/models.py b/examples/django_me_example/example/app/models.py index 4508722c3..e69de29bb 100644 --- a/examples/django_me_example/example/app/models.py +++ b/examples/django_me_example/example/app/models.py @@ -1,7 +0,0 @@ -from mongoengine.fields import ListField -from mongoengine.django.auth import User - - -class User(User): - """Extend Mongo Engine User model""" - foo = ListField(default=[]) diff --git a/examples/django_me_example/example/app/pipeline.py b/examples/django_me_example/example/app/pipeline.py index 136f8b56e..245e69cf2 100644 --- a/examples/django_me_example/example/app/pipeline.py +++ b/examples/django_me_example/example/app/pipeline.py @@ -5,10 +5,11 @@ @partial def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): - if user and user.email: + if kwargs.get('ajax') or user and user.email: return elif is_new and not details.get('email'): - if strategy.session_get('saved_email'): - details['email'] = strategy.session_pop('saved_email') + email = strategy.request_data().get('email') + if email: + details['email'] = email else: return redirect('require_email') diff --git a/examples/django_me_example/example/app/templatetags/__init__.py b/examples/django_me_example/example/app/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/django_me_example/example/app/templatetags/backend_utils.py b/examples/django_me_example/example/app/templatetags/backend_utils.py new file mode 100644 index 000000000..573b6d637 --- /dev/null +++ b/examples/django_me_example/example/app/templatetags/backend_utils.py @@ -0,0 +1,82 @@ +import re + +from django import template + +from social.backends.oauth import OAuthAuth +from social.apps.django_app.me.models import UserSocialAuth + + +register = template.Library() + +name_re = re.compile(r'([^O])Auth') + + +@register.filter +def backend_name(backend): + name = backend.__class__.__name__ + name = name.replace('OAuth', ' OAuth') + name = name.replace('OpenId', ' OpenId') + name = name.replace('Sandbox', '') + name = name_re.sub(r'\1 Auth', name) + return name + + +@register.filter +def backend_class(backend): + return backend.name.replace('-', ' ') + + +@register.filter +def icon_name(name): + return { + 'stackoverflow': 'stack-overflow', + 'google-oauth': 'google', + 'google-oauth2': 'google', + 'google-openidconnect': 'google', + 'yahoo-oauth': 'yahoo', + 'facebook-app': 'facebook', + 'email': 'envelope', + 'vimeo': 'vimeo-square', + 'linkedin-oauth2': 'linkedin', + 'vk-oauth2': 'vk', + 'live': 'windows', + 'username': 'user', + }.get(name, name) + + +@register.filter +def social_backends(backends): + backends = [(name, backend) for name, backend in backends.items() + if name not in ['username', 'email']] + backends.sort(key=lambda b: b[0]) + return [backends[n:n + 10] for n in range(0, len(backends), 10)] + + +@register.filter +def legacy_backends(backends): + backends = [(name, backend) for name, backend in backends.items() + if name in ['username', 'email']] + backends.sort(key=lambda b: b[0]) + return backends + + +@register.filter +def oauth_backends(backends): + backends = [(name, backend) for name, backend in backends.items() + if issubclass(backend, OAuthAuth)] + backends.sort(key=lambda b: b[0]) + return backends + + +@register.simple_tag(takes_context=True) +def associated(context, backend): + user = context.get('user') + context['association'] = None + if user and user.is_authenticated(): + try: + context['association'] = UserSocialAuth.objects.filter( + user=user, provider=backend.name + )[0] + except IndexError: + pass + return '' diff --git a/examples/django_me_example/example/app/views.py b/examples/django_me_example/example/app/views.py index 62eeb900f..2ed66540b 100644 --- a/examples/django_me_example/example/app/views.py +++ b/examples/django_me_example/example/app/views.py @@ -1,35 +1,74 @@ +import json + +from django.conf import settings +from django.http import HttpResponse, HttpResponseBadRequest +from django.shortcuts import redirect from django.contrib.auth.decorators import login_required -from django.template import RequestContext -from django.shortcuts import render_to_response, redirect +from django.contrib.auth import logout as auth_logout, login + +from social.backends.oauth import BaseOAuth1, BaseOAuth2 +from social.backends.google import GooglePlusAuth +from social.backends.utils import load_backends +from social.apps.django_app.utils import psa + +from example.app.decorators import render_to + + +def logout(request): + """Logs out user""" + auth_logout(request) + return redirect('/') + +def context(**extra): + return dict({ + 'plus_id': getattr(settings, 'SOCIAL_AUTH_GOOGLE_PLUS_KEY', None), + 'plus_scope': ' '.join(GooglePlusAuth.DEFAULT_SCOPE), + 'available_backends': load_backends(settings.AUTHENTICATION_BACKENDS) + }, **extra) + +@render_to('home.html') def home(request): """Home view, displays login mechanism""" if request.user.is_authenticated(): return redirect('done') - return render_to_response('home.html', {}, RequestContext(request)) + return context() @login_required +@render_to('home.html') def done(request): """Login complete view, displays user data""" - return render_to_response('done.html', {'user': request.user}, - RequestContext(request)) - - -def signup_email(request): - return render_to_response('email_signup.html', {}, RequestContext(request)) + return context() +@render_to('home.html') def validation_sent(request): - return render_to_response('validation_sent.html', { - 'email': request.session.get('email_validation_address') - }, RequestContext(request)) + return context( + validation_sent=True, + email=request.session.get('email_validation_address') + ) +@render_to('home.html') def require_email(request): - if request.method == 'POST': - request.session['saved_email'] = request.POST.get('email') - backend = request.session['partial_pipeline']['backend'] - return redirect('social:complete', backend=backend) - return render_to_response('email.html', RequestContext(request)) + backend = request.session['partial_pipeline']['backend'] + return context(email_required=True, backend=backend) + + +@psa('social:complete') +def ajax_auth(request, backend): + if isinstance(request.backend, BaseOAuth1): + token = { + 'oauth_token': request.REQUEST.get('access_token'), + 'oauth_token_secret': request.REQUEST.get('access_token_secret'), + } + elif isinstance(request.backend, BaseOAuth2): + token = request.REQUEST.get('access_token') + else: + raise HttpResponseBadRequest('Wrong backend type') + user = request.backend.do_auth(token, ajax=True) + login(request, user) + data = {'id': user.id, 'username': user.username} + return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 9c2a15740..82fd1142f 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -19,8 +19,7 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'test.db' + 'ENGINE': 'django.db.backends.dummy', } } @@ -119,11 +118,13 @@ 'social.apps.django_app.context_processors.backends', ) +AUTH_USER_MODEL = 'mongo_auth.MongoUser' +MONGOENGINE_USER_DOCUMENT = 'mongoengine.django.auth.User' SESSION_ENGINE = 'mongoengine.django.sessions' +SESSION_SERIALIZER = 'mongoengine.django.sessions.BSONSerializer' mongoengine.connect('psa', host='mongodb://localhost/psa') -MONGOENGINE_USER_DOCUMENT = 'example.app.models.User' -SOCIAL_AUTH_USER_MODEL = 'example.app.models.User' - +# MONGOENGINE_USER_DOCUMENT = 'example.app.models.User' +# SOCIAL_AUTH_USER_MODEL = 'example.app.models.User' AUTHENTICATION_BACKENDS = ( 'social.backends.open_id.OpenIdAuth', @@ -181,6 +182,7 @@ 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'social.backends.wunderlist.WunderlistOAuth2', + 'mongoengine.django.auth.MongoEngineBackend', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/examples/django_me_example/example/templates/base.html b/examples/django_me_example/example/templates/base.html deleted file mode 100644 index 86db50440..000000000 --- a/examples/django_me_example/example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/django_me_example/example/templates/done.html b/examples/django_me_example/example/templates/done.html deleted file mode 100644 index b24808267..000000000 --- a/examples/django_me_example/example/templates/done.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -

          You are logged in as {{ user.username }}!

          - -

          Associated:

          -{% for assoc in backends.associated %} -
          - {{ assoc.provider }} -
          {% csrf_token %} - -
          -
          -{% endfor %} - -

          Associate:

          -
            - {% for name in backends.not_associated %} -
          • - {{ name }} -
          • - {% endfor %} -
          -{% endblock %} diff --git a/examples/django_me_example/example/templates/email.html b/examples/django_me_example/example/templates/email.html deleted file mode 100644 index 0bf012be5..000000000 --- a/examples/django_me_example/example/templates/email.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
          - {% csrf_token %} - - - -
          -{% endblock %} diff --git a/examples/django_me_example/example/templates/email_signup.html b/examples/django_me_example/example/templates/email_signup.html deleted file mode 100644 index bb5a6ff9a..000000000 --- a/examples/django_me_example/example/templates/email_signup.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
          - {% csrf_token %} - - - - - - - - - -
          - -
          -{% endblock %} diff --git a/examples/django_me_example/example/templates/home.html b/examples/django_me_example/example/templates/home.html index 5184b12c6..3a51c5992 100644 --- a/examples/django_me_example/example/templates/home.html +++ b/examples/django_me_example/example/templates/home.html @@ -1,101 +1,440 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Strava OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box.net OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -ThisIsMyJam OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Clef OAuth
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Douban OAuth2
          -MineID OAuth2
          -Mixcloud OAuth2
          -Rdio OAuth2
          -Rdio OAuth1
          -Yammer OAuth2
          -Stackoverflow OAuth2
          -Readability OAuth1
          -Skyrock OAuth1
          -Tumblr OAuth1
          -Reddit OAuth2
          -Podio OAuth2
          -Amazon OAuth2
          -Steam OpenId
          -Email Auth
          -Username Auth
          - -
          {% csrf_token %} -
          - - - -
          -
          - -
          {% csrf_token %} -
          - - - -
          -
          - -
          - - Persona -
          -{% endblock %} - -{% block scripts %} - - - + + + + + -{% endblock %} + + $('.disconnect-form').on('click', 'a.btn', function (event) { + event.preventDefault(); + $(event.target).closest('form').submit(); + }); + + {% if validation_sent %} + $validationModal.modal('show'); + {% endif %} + + {% if email_required %} + $emailRequired.modal('show'); + {% endif %} + }); + + + diff --git a/examples/django_me_example/example/templates/username_signup.html b/examples/django_me_example/example/templates/username_signup.html deleted file mode 100644 index 343d30c01..000000000 --- a/examples/django_me_example/example/templates/username_signup.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
          - {% csrf_token %} - - - - - - - - - -
          - -
          -{% endblock %} diff --git a/examples/django_me_example/example/templates/validation_sent.html b/examples/django_me_example/example/templates/validation_sent.html deleted file mode 100644 index 6614e3e96..000000000 --- a/examples/django_me_example/example/templates/validation_sent.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -A email validation was sent to {{ email }}. -{% endblock %} diff --git a/examples/django_me_example/example/urls.py b/examples/django_me_example/example/urls.py index 9821119bc..354ab4a5d 100644 --- a/examples/django_me_example/example/urls.py +++ b/examples/django_me_example/example/urls.py @@ -7,10 +7,12 @@ urlpatterns = patterns('', url(r'^$', 'example.app.views.home'), url(r'^admin/', include(admin.site.urls)), - url(r'^signup-email/', 'example.app.views.signup_email'), url(r'^email-sent/', 'example.app.views.validation_sent'), url(r'^login/$', 'example.app.views.home'), + url(r'^logout/$', 'example.app.views.logout'), url(r'^done/$', 'example.app.views.done', name='done'), + url(r'^ajax-auth/(?P[^/]+)/$', 'example.app.views.ajax_auth', + name='ajax-auth'), url(r'^email/$', 'example.app.views.require_email', name='require_email'), url(r'', include('social.apps.django_app.urls', namespace='social')) ) diff --git a/examples/django_me_example/example/wsgi.py b/examples/django_me_example/example/wsgi.py index 9b42e63b2..4b3fb450d 100644 --- a/examples/django_me_example/example/wsgi.py +++ b/examples/django_me_example/example/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for example project. +WSGI config for dj project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable diff --git a/examples/django_me_example/requirements.txt b/examples/django_me_example/requirements.txt index 37f1c1a53..1a339e997 100644 --- a/examples/django_me_example/requirements.txt +++ b/examples/django_me_example/requirements.txt @@ -1,3 +1,3 @@ -django>=1.4 +django>=1.4,<1.8 mongoengine>=0.8.6 python-social-auth From b91a190682db776dce5b70afb884426a9eec2343 Mon Sep 17 00:00:00 2001 From: Lucas Roesler Date: Fri, 3 Apr 2015 13:17:49 -0600 Subject: [PATCH 519/890] Allow inactive users to login - Allow inactive users to login, this is controlled by the `SOCIAL_AUTH_INACTIVE_USER_LOGIN` setting. --- social/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/actions.py b/social/actions.py index 8d18bfa3c..330a51c28 100644 --- a/social/actions.py +++ b/social/actions.py @@ -74,6 +74,9 @@ def do_complete(backend, login, user=None, redirect_name='next', url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL') else: + if backend.setting('SOCIAL_AUTH_INACTIVE_USER_LOGIN', False): + social_user = user.social_user + login(backend, user, social_user) url = setting_url(backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', 'LOGIN_URL') else: From 07847e58f3e541cb3e2ce4a3e978d073288266c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 3 Apr 2015 22:43:29 -0300 Subject: [PATCH 520/890] Define a MANIFEST.in file. Fixes #578 --- MANIFEST.in | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..6a060d104 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +global-include *.py + +include *.txt Changelog LICENSE README.rst +recursive-include docs *.rst + +graft examples + +recursive-exclude social *.pyc +recursive-exclude examples *.pyc +recursive-exclude examples *.db +recursive-exclude examples local_settings.py +recursive-exclude examples/webpy_example/sessions * From 08996453dd00524789557a56cac852bfcae45a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 3 Apr 2015 23:16:35 -0300 Subject: [PATCH 521/890] Conditional import on transaction, update docs to mention it. Fixes #572 --- docs/storage.rst | 9 ++++++--- social/storage/sqlalchemy_orm.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/storage.rst b/docs/storage.rst index 59b10adaf..80e177d98 100644 --- a/docs/storage.rst +++ b/docs/storage.rst @@ -170,12 +170,14 @@ models references and implement the needed method:: raise NotImplementedError('Implement in subclass') -Sqlalchemy and Django mixins +SQLAlchemy and Django mixins ---------------------------- -Currently there are partial implementations of mixins for `Sqlalchemy ORM`_ and +Currently there are partial implementations of mixins for `SQLAlchemy ORM`_ and `Django ORM`_ with common code used later on current implemented applications. +**When using `SQLAlchemy ORM`_ and ``ZopeTransactionExtension``, it's +recommended to use the transaction_ application to handle them.** Models Examples --------------- @@ -188,10 +190,11 @@ App`_, and `Webpy App`_ for examples of implementations. .. _NonceMixin: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L149 .. _AssociationMixin: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L161 .. _BaseStorage: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L201 -.. _Sqlalchemy ORM: https://github.com/omab/python-social-auth/blob/master/social/storage/sqlalchemy_orm.py +.. _SQLAlchemy ORM: https://github.com/omab/python-social-auth/blob/master/social/storage/sqlalchemy_orm.py .. _Django ORM: https://github.com/omab/python-social-auth/blob/master/social/storage/django_orm.py .. _Django App: https://github.com/omab/python-social-auth/blob/master/social/apps/django_app/default/models.py .. _Flask App: https://github.com/omab/python-social-auth/blob/master/social/apps/flask_app/models.py .. _Pyramid App: https://github.com/omab/python-social-auth/blob/master/social/apps/pyramid_app/models.py .. _Webpy App: https://github.com/omab/python-social-auth/blob/master/social/apps/webpy_app/models.py .. _pipeline docs: pipeline.html#email-validation +.. _transaction: https://pypi.python.org/pypi/transaction diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 978d2b988..71010f74d 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -3,7 +3,10 @@ import six import json -import transaction +try: + import transaction +except ImportError: + transaction = None from sqlalchemy import Column, Integer, String from sqlalchemy.exc import IntegrityError @@ -47,8 +50,11 @@ def _flush(cls): try: cls._session().flush() except AssertionError: - with transaction.manager as manager: - manager.commit() + if transaction: + with transaction.manager as manager: + manager.commit() + else: + cls._session().commit() def save(self): self._save_instance(self) From 63a2e76dc77cffa20e5f35e07ffdd9f0a9e0124c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 00:19:05 -0300 Subject: [PATCH 522/890] Add docs about disconnection and logging out difference. Fixes #568 --- docs/index.rst | 1 + docs/logging_out.rst | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 docs/logging_out.rst diff --git a/docs/index.rst b/docs/index.rst index e585355c7..2829f7b01 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,6 +26,7 @@ Contents: storage exceptions backends/index + logging_out tests use_cases thanks diff --git a/docs/logging_out.rst b/docs/logging_out.rst new file mode 100644 index 000000000..102f00169 --- /dev/null +++ b/docs/logging_out.rst @@ -0,0 +1,25 @@ +Logging Out +=========== + +It's a common misconception that ``disconnect`` action is the same as logging +the user out, but is far from it. + +``Disconnect`` is the way that your users have to say to you "forget about my +account", that implies removing the ``UserSocialAuth`` instance that was +created, this also implies that the user won't be able to login back into your +site with the social account, instead the action will be a signup, a new user +instance will be created, not related to the previous one. + +Logging out is just a way to say "forget my current session", and usually +implies removing cookies, invalidating a session hash, etc. The many frameworks +have their own ways to logout an account (Django has ``django.contrib.auth.logout``), +``flask-login`` has it's own way too with `logout_user()`_. + +Since disconnecting a social account means that the user won't be able to log +back in with that social provider into the same user, python-social-auth will +check that the user account is in a valid state for disconnection (it has at +least one more social account associated, or a password, etc). This behavior +can be overridden by changing the `Disconnection Pipeline`_. + +.. _logout_user(): https://github.com/maxcountryman/flask-login/blob/a96de342eae560deec008a02179f593c3799b3ba/flask_login.py#L718-L739 +.. _DISCONNECT_PIPELINE: pipeline.html#disconnection-pipeline From 41a10a8008067747cf1b17f611922f8fdbb3fa64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 00:22:37 -0300 Subject: [PATCH 523/890] Link backend docs --- docs/backends/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 0859a74d2..d42e414ef 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -58,11 +58,14 @@ Social backends box clef coinbase + coursera dailymotion disqus docker douban + dribbble dropbox + eveonline evernote facebook fedora @@ -76,6 +79,7 @@ Social backends kakao khanacademy lastfm + launchpad linkedin livejournal live @@ -86,6 +90,7 @@ Social backends mineid mixcloud moves + naszaklasa nationbuilder odnoklassnikiru openstreetmap @@ -93,6 +98,7 @@ Social backends pixelpin pocket podio + qiita qq rdio readability @@ -126,3 +132,4 @@ Social backends xing yahoo yammer + zotero From 4e860bbed732707bb34a7282cb05e66969756665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 00:23:33 -0300 Subject: [PATCH 524/890] Change title --- docs/logging_out.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/logging_out.rst b/docs/logging_out.rst index 102f00169..1d737ec36 100644 --- a/docs/logging_out.rst +++ b/docs/logging_out.rst @@ -1,5 +1,5 @@ -Logging Out -=========== +Disconnect and Logging Out +========================== It's a common misconception that ``disconnect`` action is the same as logging the user out, but is far from it. From e44f1751918c33af898c6f8fdf12d419630c0d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 01:31:21 -0300 Subject: [PATCH 525/890] Remove hard limitations on PyJWT and requests-oauthlib versions. Fixes #531 --- requirements-python3.txt | 4 ++-- requirements.txt | 2 +- setup.py | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/requirements-python3.txt b/requirements-python3.txt index 586b499a5..7a856dfd9 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -1,6 +1,6 @@ python3-openid>=3.0.1 requests>=1.1.0 oauthlib>=0.3.8 -requests-oauthlib>=0.3.1,<0.3.2 +requests-oauthlib>0.3.2 six>=1.2.0 -PyJWT>=1.0.0,<2.0.0 +PyJWT>=1.0.0 diff --git a/requirements.txt b/requirements.txt index c98195be9..7f525ea7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ requests>=1.1.0 oauthlib>=0.3.8 requests-oauthlib>=0.3.1 six>=1.2.0 -PyJWT>=1.0.0,<2.0.0 +PyJWT>=1.0.0 diff --git a/setup.py b/setup.py index 3328fce42..7f857676f 100644 --- a/setup.py +++ b/setup.py @@ -46,11 +46,9 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', - 'PyJWT>=1.0.0,<2.0.0'] +requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT>=1.0.0'] if PY3: - requires += ['python3-openid>=3.0.1', - 'requests-oauthlib>=0.3.1,<0.3.2'] + requires += ['python3-openid>=3.0.1', 'requests-oauthlib>0.3.2'] else: requires += ['python-openid>=2.2', 'requests-oauthlib>=0.3.1'] From 320eb67f9c8c1a8a4deb36f42486b881d5486850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 02:07:56 -0300 Subject: [PATCH 526/890] Remove unsupported attribute from alter field migration --- .../django_app/default/migrations/0002_add_related_name.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py index d33d47aae..8e39f15bf 100644 --- a/social/apps/django_app/default/migrations/0002_add_related_name.py +++ b/social/apps/django_app/default/migrations/0002_add_related_name.py @@ -15,7 +15,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='usersocialauth', name='user', - field=models.ForeignKey(related_name='social_auth', to=settings.AUTH_USER_MODEL), - preserve_default=True, + field=models.ForeignKey(related_name='social_auth', to=settings.AUTH_USER_MODEL) ), ] From 08617e473a55cb37e81633c5a7b8ee3add50a792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 02:11:19 -0300 Subject: [PATCH 527/890] Add notice about behance broken api. Refs #530 --- docs/backends/behance.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/backends/behance.rst b/docs/backends/behance.rst index 07bed6541..6b9f1ec89 100644 --- a/docs/backends/behance.rst +++ b/docs/backends/behance.rst @@ -1,6 +1,10 @@ Behance ======= +**NOTE: IT SEEMS THAT BEHANCE HAS DROPPED THEIR OAUTH2 SUPPORT WITHOUT MUCH +NOTICE BESIDE A `BLOG POST`_ ON SEPTEMBER 2014 MENTIONING THAT IT WILL BE +INTRODUCED "SOON". THIS BACKEND IS IN DEPRECATED STATE FOR NOW.** + Behance uses OAuth2 for its auth mechanism. - Register a new application at `Behance App Registration`_, set your @@ -21,3 +25,4 @@ doc at `Behance Developer Documentation`_. .. _Behance App Registration: http://www.behance.net/dev/register .. _Possible Scopes: http://www.behance.net/dev/authentication#scopes .. _Behance Developer Documentation: http://www.behance.net/dev +.. _BLOG POST: http://blog.behance.net/dev/introducing-the-behance-api From 76f9370f2017c86240cf2366043d7ba2d95c4be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 02:14:10 -0300 Subject: [PATCH 528/890] Improve deprecation notice on behance docs --- docs/backends/behance.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/backends/behance.rst b/docs/backends/behance.rst index 6b9f1ec89..0ce8fe832 100644 --- a/docs/backends/behance.rst +++ b/docs/backends/behance.rst @@ -1,9 +1,12 @@ Behance ======= -**NOTE: IT SEEMS THAT BEHANCE HAS DROPPED THEIR OAUTH2 SUPPORT WITHOUT MUCH +DEPRECATED NOTICE +----------------- + +**NOTE:** IT SEEMS THAT BEHANCE HAS DROPPED THEIR OAUTH2 SUPPORT WITHOUT MUCH NOTICE BESIDE A `BLOG POST`_ ON SEPTEMBER 2014 MENTIONING THAT IT WILL BE -INTRODUCED "SOON". THIS BACKEND IS IN DEPRECATED STATE FOR NOW.** +INTRODUCED "SOON". THIS BACKEND IS IN DEPRECATED STATE FOR NOW. Behance uses OAuth2 for its auth mechanism. From f57e0caf4ec06f3b9a2367d9ff3897e2a3a71608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 02:44:39 -0300 Subject: [PATCH 529/890] Optional trailing slash on django apps. Fixes #505 --- social/apps/django_app/urls.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/social/apps/django_app/urls.py b/social/apps/django_app/urls.py index b0e01a6d1..add764c2d 100644 --- a/social/apps/django_app/urls.py +++ b/social/apps/django_app/urls.py @@ -8,13 +8,13 @@ urlpatterns = patterns('social.apps.django_app.views', # authentication / association - url(r'^login/(?P[^/]+)/$', 'auth', + url(r'^login/(?P[^/]+)/?$', 'auth', name='begin'), - url(r'^complete/(?P[^/]+)/$', 'complete', + url(r'^complete/(?P[^/]+)/?$', 'complete', name='complete'), # disconnection - url(r'^disconnect/(?P[^/]+)/$', 'disconnect', + url(r'^disconnect/(?P[^/]+)/?$', 'disconnect', name='disconnect'), - url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', + url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/?$', 'disconnect', name='disconnect_individual'), ) From 8aab8099a2345ccf7deaa358da3d45e1af9ef8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 03:08:04 -0300 Subject: [PATCH 530/890] Update docs about SOCIAL_AUTH_PROTECTED_USER_FIELDS. Fixes #459 --- docs/configuration/settings.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index c0bf74380..a67ddb2ed 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -255,9 +255,13 @@ Miscellaneous settings ---------------------- ``SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email',]`` - The `user_details` pipeline processor will set certain fields on user - objects, such as ``email``. Set this to a list of fields you only want to - set for newly created users and avoid updating on further logins. + During the pipeline process a ``dict`` named ``details`` will be populated + with the needed values to create the user instance, but it's also used to + update the user instance. Any value in it will be checked as an attribute + in the user instance (first by doing ``hasattr(user, name)``). Usually + there are attributes that cannot be updated (like ``username``, ``id``, + ``email``, etc.), those fields need to be *protect*. Set any field name that + requires *protection* in this setting, and it won't be updated. ``SOCIAL_AUTH_SESSION_EXPIRATION = False`` By default, user session expiration time will be set by your web @@ -268,7 +272,6 @@ Miscellaneous settings web framework's session length setting and set user session lengths to match the ``expires`` value from the auth provider. - ``SOCIAL_AUTH_OPENID_PAPE_MAX_AUTH_AGE = `` Enable `OpenID PAPE`_ extension support by defining this setting. From 7dd4273f9fcc6d12516c699f6eb8dfda8242ac8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 03:47:42 -0300 Subject: [PATCH 531/890] Improve http error handling on auth_complete/do_auth. Fixes #304 --- social/backends/beats.py | 25 ++++++++----------------- social/backends/facebook.py | 3 ++- social/backends/google.py | 24 +++++++++--------------- social/backends/jawbone.py | 23 ++++++++--------------- social/backends/lastfm.py | 2 ++ social/backends/oauth.py | 35 ++++++++++++----------------------- social/backends/persona.py | 2 ++ social/backends/pocket.py | 2 ++ social/backends/shopify.py | 9 ++------- social/backends/yahoo.py | 26 +++++++++----------------- social/exceptions.py | 6 ++++++ social/utils.py | 20 +++++++++++++++++++- 12 files changed, 81 insertions(+), 96 deletions(-) diff --git a/social/backends/beats.py b/social/backends/beats.py index d5801eb8b..d1d877252 100644 --- a/social/backends/beats.py +++ b/social/backends/beats.py @@ -4,9 +4,7 @@ """ import base64 -from requests import HTTPError - -from social.exceptions import AuthCanceled, AuthUnknownError +from social.utils import handle_http_errors from social.backends.oauth import BaseOAuth2 @@ -30,23 +28,16 @@ def auth_headers(self): )) } + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) # mashery wraps in jsonrpc if response.get('jsonrpc', None): diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 17b523818..3de7573d3 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -8,7 +8,7 @@ import base64 import hashlib -from social.utils import parse_qs, constant_time_compare +from social.utils import parse_qs, constant_time_compare, handle_http_errors from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthException, AuthCanceled, AuthUnknownError, \ AuthMissingParameter @@ -62,6 +62,7 @@ def process_error(self, data): raise AuthCanceled(self, data.get('error_message') or data.get('error_code')) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) diff --git a/social/backends/google.py b/social/backends/google.py index 253615269..a58549aa1 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -2,11 +2,10 @@ Google OpenId, OAuth2, OAuth1, Google+ Sign-in backends, docs at: http://psa.matiasaguirre.net/docs/backends/google.html """ -from requests import HTTPError - +from social.utils import handle_http_errors from social.backends.open_id import OpenIdAuth, OpenIdConnectAuth from social.backends.oauth import BaseOAuth2, BaseOAuth1 -from social.exceptions import AuthMissingParameter, AuthCanceled +from social.exceptions import AuthMissingParameter class BaseGoogleAuth(object): @@ -135,6 +134,7 @@ def auth_complete_params(self, state=None): params['redirect_uri'] = 'postmessage' return params + @handle_http_errors def auth_complete(self, *args, **kwargs): if 'access_token' in self.data and 'code' not in self.data: raise AuthMissingParameter(self, 'access_token or code') @@ -147,18 +147,12 @@ def auth_complete(self, *args, **kwargs): params={'access_token': token} )) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index cf6735904..52c84ce80 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -2,7 +2,7 @@ Jawbone OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/jawbone.html """ -from requests import HTTPError +from social.utils import handle_http_errors from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthCanceled, AuthUnknownError @@ -62,23 +62,16 @@ def auth_complete_params(self, state=None): 'client_secret': client_secret, } + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - params=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + params=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/backends/lastfm.py b/social/backends/lastfm.py index c9ea837bc..0f1acf9db 100644 --- a/social/backends/lastfm.py +++ b/social/backends/lastfm.py @@ -1,5 +1,6 @@ import hashlib +from social.utils import handle_http_errors from social.backends.base import BaseAuth @@ -21,6 +22,7 @@ class LastFmAuth(BaseAuth): def auth_url(self): return self.AUTH_URL.format(api_key=self.setting('KEY')) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" key, secret = self.get_key_and_secret() diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 717092dc0..0fa17ccdd 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -1,11 +1,10 @@ import six -from requests import HTTPError from requests_oauthlib import OAuth1 from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER from social.p3 import urlencode, unquote -from social.utils import url_add_parameters, parse_qs +from social.utils import url_add_parameters, parse_qs, handle_http_errors from social.exceptions import AuthFailed, AuthCanceled, AuthUnknownError, \ AuthMissingParameter, AuthStateMissing, \ AuthStateForbidden, AuthTokenError @@ -170,21 +169,17 @@ def process_error(self, data): raise AuthCanceled(self, 'User refused the access') raise AuthUnknownError(self, 'Error was ' + data['oauth_problem']) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Return user, might be logged in""" # Multiple unauthorized tokens are supported (see #521) self.process_error(self.data) self.validate_state() token = self.get_unauthorized_token() - try: - access_token = self.access_token(token) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise + access_token = self.access_token(token) return self.do_auth(access_token, *args, **kwargs) + @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" if not isinstance(access_token, dict): @@ -356,28 +351,22 @@ def process_error(self, data): elif 'denied' in data: raise AuthCanceled(self, data['denied']) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" state = self.validate_state() self.process_error(self.data) - try: - response = self.request_access_token( - self.access_token_url(), - data=self.auth_complete_params(state), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.access_token_url(), + data=self.auth_complete_params(state), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) + @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" data = self.user_data(access_token, *args, **kwargs) diff --git a/social/backends/persona.py b/social/backends/persona.py index 060715ca6..054ea2fed 100644 --- a/social/backends/persona.py +++ b/social/backends/persona.py @@ -2,6 +2,7 @@ Mozilla Persona authentication backend, docs at: http://psa.matiasaguirre.net/docs/backends/persona.html """ +from social.utils import handle_http_errors from social.backends.base import BaseAuth from social.exceptions import AuthFailed, AuthMissingParameter @@ -33,6 +34,7 @@ def extra_data(self, user, uid, response, details): return {'audience': response['audience'], 'issuer': response['issuer']} + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" if 'assertion' not in self.data: diff --git a/social/backends/pocket.py b/social/backends/pocket.py index 90bc5a9bf..0852d312a 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -3,6 +3,7 @@ http://psa.matiasaguirre.net/docs/backends/pocket.html """ from social.backends.base import BaseAuth +from social.utils import handle_http_errors class PocketAuth(BaseAuth): @@ -33,6 +34,7 @@ def auth_url(self): bits = (self.AUTHORIZATION_URL, token, self.redirect_uri) return '%s?request_token=%s&redirect_uri=%s' % bits + @handle_http_errors def auth_complete(self, *args, **kwargs): data = { 'consumer_key': self.setting('POCKET_CONSUMER_KEY'), diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 407856769..214fd1845 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -5,8 +5,7 @@ import imp import six -from requests import HTTPError - +from social.utils import handle_http_errors from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthFailed, AuthCanceled @@ -61,6 +60,7 @@ def auth_url(self): redirect_uri=redirect_uri ) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) @@ -73,11 +73,6 @@ def auth_complete(self, *args, **kwargs): access_token = shopify_session.token except self.shopifyAPI.ValidationException: raise AuthCanceled(self) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise else: if not access_token: raise AuthFailed(self, 'Authentication Failed') diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 7ab046db5..15fc9d17d 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -2,12 +2,11 @@ Yahoo OpenId, OAuth1 and OAuth2 backends, docs at: http://psa.matiasaguirre.net/docs/backends/yahoo.html """ -from requests import HTTPError from requests.auth import HTTPBasicAuth +from social.utils import handle_http_errors from social.backends.open_id import OpenIdAuth from social.backends.oauth import BaseOAuth2, BaseOAuth1 -from social.exceptions import AuthCanceled, AuthUnknownError class YahooOpenId(OpenIdAuth): @@ -113,24 +112,17 @@ def user_data(self, access_token, *args, **kwargs): 'Authorization': 'Bearer {0}'.format(access_token) }, method='GET')['profile'] + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - auth=HTTPBasicAuth(*self.get_key_and_secret()), - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + auth=HTTPBasicAuth(*self.get_key_and_secret()), + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/exceptions.py b/social/exceptions.py index b4b8b2f73..88e11551c 100644 --- a/social/exceptions.py +++ b/social/exceptions.py @@ -98,6 +98,12 @@ def __str__(self): return 'Your credentials aren\'t allowed' +class AuthUnreachableProvider(AuthException): + """Cannot reach the provider""" + def __str__(self): + return 'The authentication provider could not be reached' + + class InvalidEmail(AuthException): def __str__(self): return 'Email couldn\'t be validated' diff --git a/social/utils.py b/social/utils.py index 0ceb54fe8..266dc71d0 100644 --- a/social/utils.py +++ b/social/utils.py @@ -2,10 +2,13 @@ import sys import unicodedata import collections -import six +import functools +import six +import requests import social +from social.exceptions import AuthCanceled, AuthUnreachableProvider from social.p3 import urlparse, urlunparse, urlencode, \ parse_qs as battery_parse_qs @@ -187,3 +190,18 @@ def setting_url(backend, *names): value = backend.setting(name) if is_url(value): return value + + +def handle_http_errors(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except requests.HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(args[0]) + elif err.response.status_code == 503: + raise AuthUnreachableProvider(args[0]) + else: + raise + return wrapper From f9caa5ad5d67827e5c759ed11501c85c1172d101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 04:12:01 -0300 Subject: [PATCH 532/890] Pass all arguments to extra_data (save access token). Fixes #344, #416 --- social/backends/base.py | 2 +- social/backends/behance.py | 5 +++-- social/backends/disqus.py | 4 ++-- social/backends/evernote.py | 4 ++-- social/backends/exacttarget.py | 2 +- social/backends/oauth.py | 8 +++++--- social/backends/odnoklassniki.py | 2 +- social/backends/open_id.py | 4 ++-- social/backends/persona.py | 2 +- social/backends/pocket.py | 2 +- social/backends/shopify.py | 4 ++-- social/pipeline/social_auth.py | 3 ++- 12 files changed, 23 insertions(+), 19 deletions(-) diff --git a/social/backends/base.py b/social/backends/base.py index bceebb617..3d385907c 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -115,7 +115,7 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): self.strategy.clean_partial_pipeline() return out - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return deafault extra data to store in extra_data field""" data = {} for entry in (self.EXTRA_DATA or []) + self.setting('EXTRA_DATA', []): diff --git a/social/backends/behance.py b/social/backends/behance.py index bf57de001..8d98d41fe 100644 --- a/social/backends/behance.py +++ b/social/backends/behance.py @@ -30,10 +30,11 @@ def get_user_details(self, response): 'last_name': last_name, 'email': ''} - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): # Pull up the embedded user attributes so they can be found as extra # data. See the example token response for possible attributes: # http://www.behance.net/dev/authentication#step-by-step data = response.copy() data.update(response['user']) - return super(BehanceOAuth2, self).extra_data(user, uid, data, details) + return super(BehanceOAuth2, self).extra_data(user, uid, data, details, + *args, **kwargs) diff --git a/social/backends/disqus.py b/social/backends/disqus.py index c36be28d8..3542a6bb1 100644 --- a/social/backends/disqus.py +++ b/social/backends/disqus.py @@ -37,10 +37,10 @@ def get_user_details(self, response): 'name': rr.get('name', ''), } - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): meta_response = dict(response, **response.get('response', {})) return super(DisqusOAuth2, self).extra_data(user, uid, meta_response, - details) + details, *args, **kwargs) def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social/backends/evernote.py b/social/backends/evernote.py index d3eb59898..10abef7ed 100644 --- a/social/backends/evernote.py +++ b/social/backends/evernote.py @@ -54,9 +54,9 @@ def access_token(self, token): else: raise - def extra_data(self, user, uid, response, details=None): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): data = super(EvernoteOAuth, self).extra_data(user, uid, response, - details) + details, *args, **kwargs) # Evernote returns expiration timestamp in miliseconds, so it needs to # be normalized. if 'expires' in data: diff --git a/social/backends/exacttarget.py b/social/backends/exacttarget.py index ca49ac9b5..7d114becc 100644 --- a/social/backends/exacttarget.py +++ b/social/backends/exacttarget.py @@ -74,7 +74,7 @@ def auth_complete(self, *args, **kwargs): raise AuthFailed(self, 'Authentication Failed') return self.do_auth(token, *args, **kwargs) - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Load extra details from the JWT token""" data = { 'id': details.get('id'), diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 0fa17ccdd..d05b4ee9c 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -36,11 +36,13 @@ class OAuthAuth(BaseAuth): REDIRECT_STATE = False STATE_PARAMETER = False - def extra_data(self, user, uid, response, details=None): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" - data = super(OAuthAuth, self).extra_data(user, uid, response, details) - data['access_token'] = response.get('access_token', '') + data = super(OAuthAuth, self).extra_data(user, uid, response, details, + *args, **kwargs) + data['access_token'] = response.get('access_token', '') or \ + kwargs.get('access_token') return data def state_token(self): diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index f639b79b1..89cec2dc7 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -49,7 +49,7 @@ class OdnoklassnikiApp(BaseAuth): name = 'odnoklassniki-app' ID_KEY = 'uid' - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): return dict([(key, value) for key, value in response.items() if key in response['extra_data_list']]) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 63babb95d..3ab525257 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -114,7 +114,7 @@ def get_user_details(self, response): 'email': email}) return values - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return defined extra data names to store in extra_data field. Settings will be inspected to get more values names that should be stored on extra_data field. Setting name is created from current @@ -129,7 +129,7 @@ def extra_data(self, user, uid, response, details): ax_names = self.setting('AX_EXTRA_DATA') values = self.values_from_response(response, sreg_names, ax_names) from_details = super(OpenIdAuth, self).extra_data( - user, uid, {}, details + user, uid, {}, details, *args, **kwargs ) values.update(from_details) return values diff --git a/social/backends/persona.py b/social/backends/persona.py index 054ea2fed..3c288e450 100644 --- a/social/backends/persona.py +++ b/social/backends/persona.py @@ -29,7 +29,7 @@ def get_user_details(self, response): 'first_name': '', 'last_name': ''} - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return users extra data""" return {'audience': response['audience'], 'issuer': response['issuer']} diff --git a/social/backends/pocket.py b/social/backends/pocket.py index 0852d312a..bb48b71f2 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -21,7 +21,7 @@ def get_json(self, url, *args, **kwargs): def get_user_details(self, response): return {'username': response['username']} - def extra_data(self, user, uid, response, details): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): return response def auth_url(self): diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 214fd1845..35bca8021 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -36,11 +36,11 @@ def get_user_details(self, response): ) } - def extra_data(self, user, uid, response, details=None): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" data = super(ShopifyOAuth2, self).extra_data(user, uid, response, - details) + details, *args, **kwargs) session = self.shopifyAPI.Session(self.data.get('shop').strip()) # Get, and store the permanent token token = session.request_token(data['access_token'].dicts[1]) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index b790870af..87895ec10 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -83,5 +83,6 @@ def load_extra_data(backend, details, response, uid, user, *args, **kwargs): social = kwargs.get('social') or \ backend.strategy.storage.user.get_social_auth(backend.name, uid) if social: - extra_data = backend.extra_data(user, uid, response, details) + extra_data = backend.extra_data(user, uid, response, details, + *args, **kwargs) social.set_extra_data(extra_data) From 3eae711e60a025c3d02c84429adeab0911dffe9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 04:54:19 -0300 Subject: [PATCH 533/890] Log error messages. Fixes #507 --- social/apps/django_app/middleware.py | 3 +++ social/utils.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index 1d099508c..e9aaebab5 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -8,6 +8,7 @@ from django.utils.http import urlquote from social.exceptions import SocialAuthBaseException +from social.utils import social_logger class SocialAuthExceptionMiddleware(object): @@ -31,6 +32,8 @@ def process_exception(self, request, exception): backend_name = getattr(backend, 'name', 'unknown-backend') message = self.get_message(request, exception) + social_logger.error(message) + url = self.get_redirect_uri(request, exception) try: messages.error(request, message, diff --git a/social/utils.py b/social/utils.py index 266dc71d0..27982a91a 100644 --- a/social/utils.py +++ b/social/utils.py @@ -3,6 +3,7 @@ import unicodedata import collections import functools +import logging import six import requests @@ -15,6 +16,8 @@ SETTING_PREFIX = 'SOCIAL_AUTH' +social_logger = logging.Logger('social') + def import_module(name): __import__(name) From 08abcf10db77da10dd05ac2962093bdceb296470 Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Mon, 6 Apr 2015 09:51:29 -0700 Subject: [PATCH 534/890] Build a wheel, and upload with twine --- Makefile | 6 +++++- setup.cfg | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 04561bd5e..f641f8843 100644 --- a/Makefile +++ b/Makefile @@ -4,4 +4,8 @@ docs: site: docs rsync -avkz site/ tarf:sites/psa/ -.PHONY: site docs +publish: + python setup.py sdist bdist_wheel + twine upload dist/* + +.PHONY: site docs publish diff --git a/setup.cfg b/setup.cfg index 365848c3c..f345d2613 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,3 +9,6 @@ with-coverage=1 cover-erase=1 cover-package=social rednose=1 + +[wheel] +universal = 1 From fa45d0ecf2ba450de36908e2c3ad94c89881eeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 6 Apr 2015 16:41:43 -0300 Subject: [PATCH 535/890] Flag dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 6baaa950a..b7c376a21 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 3) -extra = '' +version = (0, 2, 4) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From e67b622d57f850d8157d2e6ff2fd49d209b01918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Apr 2015 13:17:27 -0300 Subject: [PATCH 536/890] Raise error if token was passed but it's incomplete. Fixes #574 --- social/backends/oauth.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index d05b4ee9c..2b6dfbd22 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -265,14 +265,23 @@ def oauth_auth(self, token=None, oauth_verifier=None, signature_type=SIGNATURE_TYPE_AUTH_HEADER): key, secret = self.get_key_and_secret() oauth_verifier = oauth_verifier or self.data.get('oauth_verifier') - token = token or {} + if token: + resource_owner_key = token.get('oauth_token') + resource_owner_secret = token.get('oauth_token_secret') + if not resource_owner_key: + raise AuthTokenError(self, 'Missing oauth_token') + if not resource_owner_secret: + raise AuthTokenError(self, 'Missing oauth_token_secret') + else: + resource_owner_key = None + resource_owner_secret = None # decoding='utf-8' produces errors with python-requests on Python3 # since the final URL will be of type bytes decoding = None if six.PY3 else 'utf-8' state = self.get_or_create_state() return OAuth1(key, secret, - resource_owner_key=token.get('oauth_token'), - resource_owner_secret=token.get('oauth_token_secret'), + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret, callback_uri=self.get_redirect_uri(state), verifier=oauth_verifier, signature_type=signature_type, From 934f159e2923ed263044fe5f09ddfd7af9da56d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Apr 2015 19:48:39 -0300 Subject: [PATCH 537/890] Fix get_scope() override example --- docs/use_cases.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index ac12fa269..e1ef0a7ed 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -163,7 +163,7 @@ accomplish that behavior, there are two ways to do it. def get_scope(self): scope = super(CustomFacebookOAuth2, self).get_scope() if self.data.get('extrascope'): - scope += [('foo', 'bar')] + scope = scope + [('foo', 'bar')] return scope @@ -175,6 +175,19 @@ accomplish that behavior, there are two ways to do it. Put this new backend in some place in your project and replace the original ``FacebookOAuth2`` in ``AUTHENTICATION_BACKENDS`` with this new version. + When overriding this method, take into account that the default output the + base class for ``get_scope()`` is the raw value from the settings (whatever + they are defined), doing this will actually update the value in your + settings for all the users:: + + scope = super(CustomFacebookOAuth2, self).get_scope() + scope += ['foo', 'bar'] + + Instead do it like this:: + + scope = super(CustomFacebookOAuth2, self).get_scope() + scope = scope + ['foo', 'bar'] + 2. It's possible to do the same by defining a second backend which extends from the original but overrides the name, this will imply new URLs and also new settings for the new backend (since the name is used to build the settings From d61012d06805b5886ac8ced803ddcb9211a0f8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Apr 2015 22:53:22 -0300 Subject: [PATCH 538/890] Update Facebook to API v2.3 Fixes #480 --- social/backends/facebook.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 3de7573d3..f3a3b0744 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -19,11 +19,11 @@ class FacebookOAuth2(BaseOAuth2): name = 'facebook' RESPONSE_TYPE = None SCOPE_SEPARATOR = ',' - AUTHORIZATION_URL = 'https://www.facebook.com/dialog/oauth' - ACCESS_TOKEN_URL = 'https://graph.facebook.com/oauth/access_token' - REVOKE_TOKEN_URL = 'https://graph.facebook.com/{uid}/permissions' + AUTHORIZATION_URL = 'https://www.facebook.com/v2.3/dialog/oauth' + ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.3/oauth/access_token' + REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.3/{uid}/permissions' REVOKE_TOKEN_METHOD = 'DELETE' - USER_DATA_URL = 'https://graph.facebook.com/me' + USER_DATA_URL = 'https://graph.facebook.com/v2.3/me' EXTRA_DATA = [ ('id', 'id'), ('expires', 'expires') @@ -71,7 +71,7 @@ def auth_complete(self, *args, **kwargs): state = self.validate_state() key, secret = self.get_key_and_secret() url = self.ACCESS_TOKEN_URL - response = self.get_querystring(url, params={ + response = self.get_json(url, params={ 'client_id': key, 'redirect_uri': self.get_redirect_uri(state), 'client_secret': secret, From 19dafb5893ba12be6dd9002cbe19f6933575dad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Apr 2015 22:59:10 -0300 Subject: [PATCH 539/890] Fix Facebook test case after API version change. Refs #480 --- social/tests/backends/test_facebook.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py index 6fe7aa66d..fc1aa5feb 100644 --- a/social/tests/backends/test_facebook.py +++ b/social/tests/backends/test_facebook.py @@ -1,6 +1,5 @@ import json -from social.p3 import urlencode from social.exceptions import AuthUnknownError from social.tests.backends.oauth import OAuth2Test @@ -8,9 +7,9 @@ class FacebookOAuth2Test(OAuth2Test): backend_path = 'social.backends.facebook.FacebookOAuth2' - user_data_url = 'https://graph.facebook.com/me' + user_data_url = 'https://graph.facebook.com/v2.3/me' expected_username = 'foobar' - access_token_body = urlencode({ + access_token_body = json.dumps({ 'access_token': 'foobar', 'token_type': 'bearer' }) From 6bd98becba0a2bfa647676c0e8265e9066c9ef25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 7 Apr 2015 23:07:31 -0300 Subject: [PATCH 540/890] Fix links in docs --- docs/backends/dribbble.rst | 9 ++++++--- docs/backends/eveonline.rst | 2 ++ docs/backends/qiita.rst | 12 ++++++++---- docs/backends/slack.rst | 12 ++++++++---- docs/backends/yahoo.rst | 3 +++ docs/logging_out.rst | 2 +- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/backends/dribbble.rst b/docs/backends/dribbble.rst index 0bf2b99c4..64333e9f3 100644 --- a/docs/backends/dribbble.rst +++ b/docs/backends/dribbble.rst @@ -3,8 +3,8 @@ Dribbble Dribbble -- Register a new application at `https://dribbble.com/account/applications/new`_, set the - callback URL to ``http://example.com/complete/dribbble/`` replacing +- Register a new application at Dribbble_, set the callback URL + to ``http://example.com/complete/dribbble/`` replacing ``example.com`` with your domain. - Fill ``Client ID`` and ``Client Secret`` values in the settings:: @@ -16,5 +16,8 @@ Dribbble SOCIAL_AUTH_DRIBBBLE_SCOPE = [...] - See auth scopes at http://developer.dribbble.com/v1/oauth/ + See auth scopes at `Dribbble Developer docs`_. + +.. _Dribbble: https://dribbble.com/account/applications/new +.. _Dribbble Developer docs: http://developer.dribbble.com/v1/oauth/ diff --git a/docs/backends/eveonline.rst b/docs/backends/eveonline.rst index 8a85a82f6..7c0503746 100644 --- a/docs/backends/eveonline.rst +++ b/docs/backends/eveonline.rst @@ -19,3 +19,5 @@ The EVE Single Sign-On (SSO) works similar to GitHub (OAuth2). - If you want to access EVE Online's CREST API, use:: SOCIAL_AUTH_EVEONLINE_SCOPE = ['publicData'] + +.. _EVE Developers: https://developers.eveonline.com/ diff --git a/docs/backends/qiita.rst b/docs/backends/qiita.rst index b1cd5dd2b..512ca9d9f 100644 --- a/docs/backends/qiita.rst +++ b/docs/backends/qiita.rst @@ -3,9 +3,9 @@ Qiita Qiita -- Register a new application at `https://qiita.com/settings/applications`_, set the - callback URL to ``http://example.com/complete/qiita/`` replacing - ``example.com`` with your domain. +- Register a new application at Qiita_, set the callback URL to + ``http://example.com/complete/qiita/`` replacing ``example.com`` with your + domain. - Fill ``Client ID`` and ``Client Secret`` values in the settings:: @@ -16,4 +16,8 @@ Qiita SOCIAL_AUTH_QIITA_SCOPE = [...] - See auth scopes at https://qiita.com/api/v2/docs#スコープ + See auth scopes at `Qiita Scopes docs`_. + + +.. _Qiita: https://qiita.com/settings/applications +.. _Qiita Scopes docs: https://qiita.com/api/v2/docs#スコープ diff --git a/docs/backends/slack.rst b/docs/backends/slack.rst index 831a9deeb..ce3ed29aa 100644 --- a/docs/backends/slack.rst +++ b/docs/backends/slack.rst @@ -3,9 +3,9 @@ Slack Slack -- Register a new application at `https://api.slack.com/applications`_, set the - callback URL to ``http://example.com/complete/slack/`` replacing - ``example.com`` with your domain. +- Register a new application at Slack_, set the callback URL to + ``http://example.com/complete/slack/`` replacing ``example.com`` with your + domain. - Fill ``Client ID`` and ``Client Secret`` values in the settings:: @@ -16,4 +16,8 @@ Slack SOCIAL_AUTH_SLACK_SCOPE = [...] - See auth scopes at https://api.slack.com/docs/oauth + See auth scopes at `Slack OAuth docs`_. + + +.. _Slack: https://api.slack.com/applications +.. _Slack OAuth docs: https://api.slack.com/docs/oauth diff --git a/docs/backends/yahoo.rst b/docs/backends/yahoo.rst index ca002f694..01a0c7e37 100644 --- a/docs/backends/yahoo.rst +++ b/docs/backends/yahoo.rst @@ -28,3 +28,6 @@ OAuth 2.0 workflow, useful if you are planning to use Yahoo's API. SOCIAL_AUTH_YAHOO_OAUTH2_KEY = '' SOCIAL_AUTH_YAHOO_OAUTH2_SECRET = '' + + +.. _Yahoo Developer Center: https://developer.yahoo.com/ diff --git a/docs/logging_out.rst b/docs/logging_out.rst index 1d737ec36..aed9ed663 100644 --- a/docs/logging_out.rst +++ b/docs/logging_out.rst @@ -22,4 +22,4 @@ least one more social account associated, or a password, etc). This behavior can be overridden by changing the `Disconnection Pipeline`_. .. _logout_user(): https://github.com/maxcountryman/flask-login/blob/a96de342eae560deec008a02179f593c3799b3ba/flask_login.py#L718-L739 -.. _DISCONNECT_PIPELINE: pipeline.html#disconnection-pipeline +.. _Disconnection Pipeline: pipeline.html#disconnection-pipeline From 3324b3f4c429e1bf569e0a839cd852acd37ea1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 8 Apr 2015 04:06:37 -0300 Subject: [PATCH 541/890] Fix settings names on spotify docs. Fixes #475 --- docs/backends/spotify.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backends/spotify.rst b/docs/backends/spotify.rst index 3f1c0dd01..ca3ddd1f6 100644 --- a/docs/backends/spotify.rst +++ b/docs/backends/spotify.rst @@ -19,7 +19,7 @@ Add the Spotify OAuth2 backend to your settings page:: - Fill ``App Key`` and ``App Secret`` values in the settings:: - SOCIAL_AUTH_SPOTIFY_OAUTH2_KEY = '' - SOCIAL_AUTH_SPOTIFY_OAUTH2_SECRET = '' + SOCIAL_AUTH_SPOTIFY_KEY = '' + SOCIAL_AUTH_SPOTIFY_SECRET = '' .. _Spotify Web API: https://developer.spotify.com/spotify-web-api From 29a24e1c43d5d7c71e0c3c07c6e28602c08b52f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 8 Apr 2015 04:15:24 -0300 Subject: [PATCH 542/890] Move revoke methods to common class. Fixes #484 --- social/backends/google.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/social/backends/google.py b/social/backends/google.py index a58549aa1..bc37d2d26 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -73,6 +73,12 @@ def user_data(self, access_token, *args, **kwargs): 'alt': 'json' }) + def revoke_token_params(self, token, uid): + return {'token': token} + + def revoke_token_headers(self, token, uid): + return {'Content-type': 'application/json'} + class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): """Google OAuth2 authentication backend""" @@ -95,12 +101,6 @@ class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): ('token_type', 'token_type', True) ] - def revoke_token_params(self, token, uid): - return {'token': token} - - def revoke_token_headers(self, token, uid): - return {'Content-type': 'application/json'} - class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): name = 'google-plus' From 454292efe8b9e72d5079f113105aa09ba32f2ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 10 Apr 2015 13:00:17 -0300 Subject: [PATCH 543/890] Link to post about access-token based authentication --- docs/use_cases.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index e1ef0a7ed..8216876f4 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -144,6 +144,9 @@ will be done by AJAX. It doesn't return the user information, but that's something that can be extended and filled to suit the project where it's going to be used. +This topic is well addressed in `A Rest API using Django and authentication +with OAuth2 AND third parties!`_ wrote by `Félix Descôteaux`_. + Multiple scopes per provider ---------------------------- @@ -311,3 +314,5 @@ Set this pipeline after ``social_user``:: .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list +.. _Félix Descôteaux: https://twitter.com/FelixDescoteaux +.. _A Rest API using Django and authentication with OAuth2 AND third parties!: http://httplambda.com/a-rest-api-with-django-and-oauthw-authentication/ From 2c1e6d6c71668a2370c23bdff199536de67d3c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 11 Apr 2015 04:00:16 -0300 Subject: [PATCH 544/890] Fix setting name (make it backend related). Refs #586 --- social/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/actions.py b/social/actions.py index 330a51c28..59a4d6d98 100644 --- a/social/actions.py +++ b/social/actions.py @@ -74,7 +74,7 @@ def do_complete(backend, login, user=None, redirect_name='next', url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL') else: - if backend.setting('SOCIAL_AUTH_INACTIVE_USER_LOGIN', False): + if backend.setting('INACTIVE_USER_LOGIN', False): social_user = user.social_user login(backend, user, social_user) url = setting_url(backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', From c73ad1fbe45121bf00ee56c2bef27b3d0475e542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 11 Apr 2015 22:40:44 -0300 Subject: [PATCH 545/890] v0.2.4 --- Changelog | 101 ++++++++++++++++++++++++++++++++++++++++++++- social/__init__.py | 2 +- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 17d3cb363..7dd032b8e 100644 --- a/Changelog +++ b/Changelog @@ -1,7 +1,99 @@ -2015-03-31 HEAD (unreleased) +2015-04-11 v0.2.4 +================= + + * 2015-04-11 Matías Aguirre + Fix setting name (make it backend related). Refs #586 + + * 2015-04-10 Matías Aguirre + Link to post about access-token based authentication + + * 2015-04-08 Matías Aguirre + Move revoke methods to common class. Fixes #484 + + * 2015-04-08 Matías Aguirre + Fix settings names on spotify docs. Fixes #475 + + * 2015-04-07 Matías Aguirre + Fix links in docs + + * 2015-04-07 Matías Aguirre + Fix Facebook test case after API version change. Refs #480 + + * 2015-04-07 Matías Aguirre + Update Facebook to API v2.3 + + * 2015-04-07 Matías Aguirre + Fix get_scope() override example + + * 2015-04-07 Matías Aguirre + Raise error if token was passed but it's incomplete. Fixes #574 + + * 2015-04-06 Matías Aguirre + Flag dev version + + * 2015-04-06 Matt Robenolt + Build a wheel, and upload with twine + + * 2015-04-04 Matías Aguirre + Log error messages. Fixes #507 + + * 2015-04-04 Matías Aguirre + Pass all arguments to extra_data (save access token). + + * 2015-04-04 Matías Aguirre + Improve http error handling on auth_complete/do_auth. Fixes #304 + + * 2015-04-04 Matías Aguirre + Update docs about SOCIAL_AUTH_PROTECTED_USER_FIELDS. Fixes #459 + + * 2015-04-04 Matías Aguirre + Optional trailing slash on django apps. Fixes #505 + + * 2015-04-04 Matías Aguirre + Improve deprecation notice on behance docs + + * 2015-04-04 Matías Aguirre + Add notice about behance broken api. Refs #530 + + * 2015-04-04 Matías Aguirre + Remove unsupported attribute from alter field migration + + * 2015-04-04 Matías Aguirre + Remove hard limitations on PyJWT and requests-oauthlib versions. Fixes #531 + + * 2015-04-04 Matías Aguirre + Change title + + * 2015-04-04 Matías Aguirre + Link backend docs + + * 2015-04-04 Matías Aguirre + Add docs about disconnection and logging out difference. Fixes #568 + + * 2015-04-03 Matías Aguirre + Conditional import on transaction, update docs to mention it. Fixes #572 + + * 2015-04-03 Matías Aguirre + Define a MANIFEST.in file. Fixes #578 + + * 2015-04-03 Lucas Roesler + Allow inactive users to login + + * 2015-04-02 Matías Aguirre + Update django/mongoengine example (similar to default one). Refs #576 + + * 2015-04-02 Matías Aguirre + Add backward compatibility on django app initialization. Refs #550 + + * 2015-04-01 M.Yasoob Ullah Khalid ☺ + Update LICENSE + 2015-03-31 v0.2.3 ================= + * 2015-03-31 Matías Aguirre + v0.2.3 + * 2015-03-31 Matías Aguirre PEP8. Refs #570 @@ -53,6 +145,9 @@ * 2015-03-17 Matías Aguirre Ensure to flush the db session (needed for Pyramid + sqlalchemy). Refs #390 + * 2015-03-12 DanielJDufour + update for django 1.9 + * 2015-03-12 Matt Howland Create vend.py @@ -280,6 +375,10 @@ * 2014-12-24 Alex Muller Update GitHub documentation + * 2014-12-19 Frankie Robertson + Fix #460: Call force_text on _URL settings to support reverse_lazy with + default session serializer + * 2014-11-27 James Potter Update django.rst diff --git a/social/__init__.py b/social/__init__.py index b7c376a21..78737f85a 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 4) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 50aafbff1a196d3ab36fa6a7e2594aee29fed4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 13 Apr 2015 00:28:38 -0300 Subject: [PATCH 546/890] Add email to default list of protected userfields (popular demand) --- social/pipeline/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 1e9971cbb..7e173a840 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -73,7 +73,7 @@ def user_details(strategy, details, user=None, *args, **kwargs): """Update user details using data from provider.""" if user: changed = False # flag to track changes - protected = ('username', 'id', 'pk') + \ + protected = ('username', 'id', 'pk', 'email') + \ tuple(strategy.setting('PROTECTED_USER_FIELDS', [])) # Update user model attributes with the new data sent by the current From fe76417d1b3b990539e0dc634f2585f34ce2d4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 13 Apr 2015 01:55:05 -0300 Subject: [PATCH 547/890] Fix wheel support. Refs #588 --- Makefile | 15 +++++++++--- setup.cfg | 3 --- setup.py | 72 ++++++++++++++++++++++++++++++------------------------- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/Makefile b/Makefile index f641f8843..b623b4e8a 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,17 @@ docs: site: docs rsync -avkz site/ tarf:sites/psa/ -publish: - python setup.py sdist bdist_wheel - twine upload dist/* +build: + python setup.py sdist + python setup.py bdist_wheel --python-tag py2 + BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3 + +publish: build + python setup.py upload + +clean: + find . -name '*.py[co]' -delete + find . -name '__pycache__' -delete + rm -rf python_social_auth.egg-info dist build .PHONY: site docs publish diff --git a/setup.cfg b/setup.cfg index f345d2613..365848c3c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,3 @@ with-coverage=1 cover-erase=1 cover-package=social rednose=1 - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index 7f857676f..c99f3b4d7 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup -PY3 = sys.version_info[0] == 3 +PY3 = os.environ.get('BUILD_VERSION') == '3' or sys.version_info[0] == 3 version = __import__('social').__version__ @@ -46,34 +46,42 @@ def get_packages(): return packages -requires = ['requests>=1.1.0', 'oauthlib>=0.3.8', 'six>=1.2.0', 'PyJWT>=1.0.0'] -if PY3: - requires += ['python3-openid>=3.0.1', 'requests-oauthlib>0.3.2'] -else: - requires += ['python-openid>=2.2', 'requests-oauthlib>=0.3.1'] - - -setup(name='python-social-auth', - version=version, - author='Matias Aguirre', - author_email='matiasaguirre@gmail.com', - description='Python social authentication made simple.', - license='BSD', - keywords='django, flask, pyramid, webpy, openid, oauth, social auth', - url='https://github.com/omab/python-social-auth', - packages=get_packages(), - # package_data={'social': ['locale/*/LC_MESSAGES/*']}, - long_description=long_description(), - install_requires=requires, - classifiers=['Development Status :: 4 - Beta', - 'Topic :: Internet', - 'License :: OSI Approved :: BSD License', - 'Intended Audience :: Developers', - 'Environment :: Web Environment', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3'], - tests_require=['sure>=1.2.5', 'httpretty>=0.8.0', 'mock>=1.0.1'], - test_suite='social.tests', - zip_safe=False) +requirements_file, tests_requirements_file = { + False: ('requirements.txt', 'social/tests/requirements.txt'), + True: ('requirements-python3.txt', 'social/tests/requirements-python3.txt') +}[PY3] + +with open(requirements_file, 'r') as f: + requirements = f.readlines() + +with open(tests_requirements_file, 'r') as f: + tests_requirements = f.readlines() + +setup( + name='python-social-auth', + version=version, + author='Matias Aguirre', + author_email='matiasaguirre@gmail.com', + description='Python social authentication made simple.', + license='BSD', + keywords='django, flask, pyramid, webpy, openid, oauth, social auth', + url='https://github.com/omab/python-social-auth', + packages=get_packages(), + # package_data={'social': ['locale/*/LC_MESSAGES/*']}, + long_description=long_description(), + install_requires=requirements, + classifiers=[ + 'Development Status :: 4 - Beta', + 'Topic :: Internet', + 'License :: OSI Approved :: BSD License', + 'Intended Audience :: Developers', + 'Environment :: Web Environment', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3' + ], + tests_require=tests_requirements, + test_suite='social.tests', + zip_safe=False +) From 74535c687c3373a9f58cd0e310ed29980200ab91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 13 Apr 2015 01:57:19 -0300 Subject: [PATCH 548/890] v0.2.5 --- Changelog | 16 ++++++++++++++-- social/__init__.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 7dd032b8e..e7b0b1d05 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,18 @@ +2015-04-13 v0.2.5 +================= + + * 2015-04-13 Matías Aguirre + Fix wheel support. Refs #588 + + * 2015-04-13 Matías Aguirre + Add email to default list of protected userfields (popular demand) + 2015-04-11 v0.2.4 ================= + * 2015-04-11 Matías Aguirre + v0.2.4 + * 2015-04-11 Matías Aguirre Fix setting name (make it backend related). Refs #586 @@ -618,7 +630,7 @@ * 2014-09-11 Matías Aguirre Flag dev version -2014-09-11 v0.2.1 +2015-09-11 v0.2.1 ================= * 2014-09-11 Matías Aguirre @@ -633,7 +645,7 @@ * 2014-09-11 Matías Aguirre Flag dev version -2014-09-11 v0.2.0 +2015-09-11 v0.2.0 ================= * 2014-09-11 Matías Aguirre diff --git a/social/__init__.py b/social/__init__.py index 78737f85a..6bc2b8453 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 4) +version = (0, 2, 5) extra = '' __version__ = '.'.join(map(str, version)) + extra From 56a24af2ca18b7be0b0ce198de99ad9810120924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 13 Apr 2015 02:03:28 -0300 Subject: [PATCH 549/890] Fix publish task --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b623b4e8a..03494f7f3 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,10 @@ build: python setup.py bdist_wheel --python-tag py2 BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3 -publish: build - python setup.py upload +publish: + python setup.py sdist upload + python setup.py bdist_wheel --python-tag py2 upload + BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3 upload clean: find . -name '*.py[co]' -delete From 859225f804952e2b8f9a541da0a6f0d49edb5ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 14 Apr 2015 17:03:12 -0300 Subject: [PATCH 550/890] Include tests requirements files. Fixes #590 --- MANIFEST.in | 1 + setup.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 6a060d104..7675886d6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ global-include *.py include *.txt Changelog LICENSE README.rst recursive-include docs *.rst +recursive-include social/tests *.txt graft examples diff --git a/setup.py b/setup.py index c99f3b4d7..8860b0a0b 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,6 @@ def get_packages(): keywords='django, flask, pyramid, webpy, openid, oauth, social auth', url='https://github.com/omab/python-social-auth', packages=get_packages(), - # package_data={'social': ['locale/*/LC_MESSAGES/*']}, long_description=long_description(), install_requires=requirements, classifiers=[ @@ -81,6 +80,10 @@ def get_packages(): 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3' ], + package_data={ + 'social/tests': ['social/tests/*.txt'] + }, + include_package_data=True, tests_require=tests_requirements, test_suite='social.tests', zip_safe=False From f0a06df0f8f5c332fe2a6b1ee439a47e94e19cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 14 Apr 2015 17:04:45 -0300 Subject: [PATCH 551/890] v0.2.6 --- Changelog | 16 ++++++++++++++-- social/__init__.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index e7b0b1d05..f93ad700d 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,18 @@ +2015-04-14 v0.2.6 +================= + + * 2015-04-14 Matías Aguirre + Include tests requirements files. Fixes #590 + + * 2015-04-13 Matías Aguirre + Fix publish task + 2015-04-13 v0.2.5 ================= + * 2015-04-13 Matías Aguirre + v0.2.5 + * 2015-04-13 Matías Aguirre Fix wheel support. Refs #588 @@ -630,7 +642,7 @@ * 2014-09-11 Matías Aguirre Flag dev version -2015-09-11 v0.2.1 +2014-09-11 v0.2.1 ================= * 2014-09-11 Matías Aguirre @@ -645,7 +657,7 @@ * 2014-09-11 Matías Aguirre Flag dev version -2015-09-11 v0.2.0 +2014-09-11 v0.2.0 ================= * 2014-09-11 Matías Aguirre diff --git a/social/__init__.py b/social/__init__.py index 6bc2b8453..4b9d3fb22 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 5) +version = (0, 2, 6) extra = '' __version__ = '.'.join(map(str, version)) + extra From 9f459869c74e8f3ef91c81291203417a6ea5618b Mon Sep 17 00:00:00 2001 From: Christian Pedersen Date: Wed, 15 Apr 2015 13:18:30 +0200 Subject: [PATCH 552/890] Append trailing slash in Django This commit https://github.com/omab/python-social-auth/commit/f57e0caf4ec06f3b9a2367d9ff3897e2a3a71608 made trailing slashes optional. The side effect of that is that the redirect_uri is created without a trailing slash. With this change the standard Django APPEND_SLASH is checked when generating the redirect url. --- social/apps/django_app/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/apps/django_app/utils.py b/social/apps/django_app/utils.py index ff950a9c7..ae20098c7 100644 --- a/social/apps/django_app/utils.py +++ b/social/apps/django_app/utils.py @@ -37,6 +37,8 @@ def wrapper(request, backend, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = reverse(redirect_uri, args=(backend,)) + if settings.APPEND_SLASH and not uri.endswith('/'): + uri = uri + '/' request.social_strategy = load_strategy(request) # backward compatibility in attribute name, only if not already From 6eed1dbab510eb96bc95af4de38fb33f0b45f07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 15 Apr 2015 11:54:15 -0300 Subject: [PATCH 553/890] Swtich Twitter API to POST (as it's documented) --- social/backends/twitter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index ed080eaf3..c41f15ab1 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -10,6 +10,8 @@ class TwitterOAuth(BaseOAuth1): """Twitter OAuth authentication backend""" name = 'twitter' EXTRA_DATA = [('id', 'id')] + REQUEST_TOKEN_METHOD = 'POST' + ACCESS_TOKEN_METHOD = 'POST' AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authenticate' REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' From a8b2790b595a19949a7157c3103fd64d75b93f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 15 Apr 2015 11:56:43 -0300 Subject: [PATCH 554/890] Flag dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 4b9d3fb22..49066c214 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 6) -extra = '' +version = (0, 2, 7) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 93969ed63ac2b7ca5deffda17fc356ffb8b4dd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 15 Apr 2015 12:48:53 -0300 Subject: [PATCH 555/890] Clean any pipeline remanents when starting the process. Refs #325 --- social/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/actions.py b/social/actions.py index 59a4d6d98..4b005cf99 100644 --- a/social/actions.py +++ b/social/actions.py @@ -4,6 +4,9 @@ def do_auth(backend, redirect_name='next'): + # Clean any partial pipeline data + backend.strategy.clean_partial_pipeline() + # Save any defined next value into session data = backend.strategy.request_data(merge=False) From 4f3b63ec875e08eadf93016969c572ac23bf684f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 15 Apr 2015 18:16:16 -0300 Subject: [PATCH 556/890] Remove single-use var --- social/backends/facebook.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index f3a3b0744..dc10d4c39 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -70,8 +70,7 @@ def auth_complete(self, *args, **kwargs): raise AuthMissingParameter(self, 'code') state = self.validate_state() key, secret = self.get_key_and_secret() - url = self.ACCESS_TOKEN_URL - response = self.get_json(url, params={ + response = self.get_json(self.ACCESS_TOKEN_URL, params={ 'client_id': key, 'redirect_uri': self.get_redirect_uri(state), 'client_secret': secret, From 663a174db194b1cc46effcaa4a2dc45d41d0d8e3 Mon Sep 17 00:00:00 2001 From: Jones Chi Date: Thu, 16 Apr 2015 11:40:20 +0800 Subject: [PATCH 557/890] Alter email max length for Django app Django 1.8 starts to set max length of EmailField to 254. --- .../migrations/0003_alter_email_max_length.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 social/apps/django_app/default/migrations/0003_alter_email_max_length.py diff --git a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py new file mode 100644 index 000000000..e9cb54043 --- /dev/null +++ b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('default', '0002_add_related_name'), + ] + + operations = [ + migrations.AlterField( + model_name='code', + name='email', + field=models.EmailField(max_length=254), + ), + ] From c50aed197c3637be785e9fa47571bbe384d72fbe Mon Sep 17 00:00:00 2001 From: zz Date: Thu, 16 Apr 2015 17:45:11 +0800 Subject: [PATCH 558/890] Fix the final_username may be empty and will skip the loop. --- social/pipeline/user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 7e173a840..0fac0ac9a 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -45,7 +45,8 @@ def get_username(strategy, details, user=None, *args, **kwargs): # Generate a unique username for current user using username # as base but adding a unique hash at the end. Original # username is cut to avoid any field max_length. - while storage.user.user_exists(username=final_username): + # The final_username may be empty and will skip the loop. + while storage.user.user_exists(username=final_username) or not final_username: username = short_username + uuid4().hex[:uuid_length] final_username = slug_func(clean_func(username[:max_length])) else: From 05b251c7c9a8283d86c5dff6f7d43e6793215456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Apr 2015 20:20:35 -0300 Subject: [PATCH 559/890] Move OAuth1 method out from the base class --- social/backends/base.py | 4 ---- social/backends/oauth.py | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/social/backends/base.py b/social/backends/base.py index 3d385907c..fcb910823 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -191,10 +191,6 @@ def continue_pipeline(self, *args, **kwargs): kwargs.update({'backend': self, 'strategy': self.strategy}) return self.authenticate(*args, **kwargs) - def request_token_extra_arguments(self): - """Return extra arguments needed on request-token process""" - return self.setting('REQUEST_TOKEN_EXTRA_ARGUMENTS', {}) - def auth_extra_arguments(self): """Return extra arguments needed on auth process. The defaults can be overriden by GET parameters.""" diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 2b6dfbd22..d15460669 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -224,6 +224,10 @@ def set_unauthorized_token(self): self.strategy.session_set(name, tokens) return token + def request_token_extra_arguments(self): + """Return extra arguments needed on request-token process""" + return self.setting('REQUEST_TOKEN_EXTRA_ARGUMENTS', {}) + def unauthorized_token(self): """Return request for unauthorized token (first stage)""" params = self.request_token_extra_arguments() From a3e180532f26f5efd65092437457b0210e199264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Apr 2015 20:34:07 -0300 Subject: [PATCH 560/890] Take into account that sometimes API v2.3 returns the old querystring format. Fixes #592 --- social/backends/facebook.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index dc10d4c39..b887d11e6 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -70,12 +70,19 @@ def auth_complete(self, *args, **kwargs): raise AuthMissingParameter(self, 'code') state = self.validate_state() key, secret = self.get_key_and_secret() - response = self.get_json(self.ACCESS_TOKEN_URL, params={ + response = self.request(self.ACCESS_TOKEN_URL, params={ 'client_id': key, 'redirect_uri': self.get_redirect_uri(state), 'client_secret': secret, 'code': self.data['code'] }) + # API v2.3 returns a JSON, according to the documents linked at issue + # #592, but it seems that this needs to be enabled(?), otherwise the + # usual querystring type response is returned. + try: + response = response.json() + except ValueError: + response = parse_qs(response.text) access_token = response['access_token'] return self.do_auth(access_token, response, *args, **kwargs) From 4b4b9b9ebc0f3a3b3c45d156b21d86b6f7a15d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Apr 2015 20:43:29 -0300 Subject: [PATCH 561/890] PEP8 and switch check order --- social/pipeline/user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 0fac0ac9a..f4fcd643e 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -46,7 +46,8 @@ def get_username(strategy, details, user=None, *args, **kwargs): # as base but adding a unique hash at the end. Original # username is cut to avoid any field max_length. # The final_username may be empty and will skip the loop. - while storage.user.user_exists(username=final_username) or not final_username: + while not final_username or \ + storage.user.user_exists(username=final_username): username = short_username + uuid4().hex[:uuid_length] final_username = slug_func(clean_func(username[:max_length])) else: @@ -59,8 +60,8 @@ def create_user(strategy, details, user=None, *args, **kwargs): return {'is_new': False} fields = dict((name, kwargs.get(name) or details.get(name)) - for name in strategy.setting('USER_FIELDS', - USER_FIELDS)) + for name in strategy.setting('USER_FIELDS', + USER_FIELDS)) if not fields: return @@ -75,7 +76,7 @@ def user_details(strategy, details, user=None, *args, **kwargs): if user: changed = False # flag to track changes protected = ('username', 'id', 'pk', 'email') + \ - tuple(strategy.setting('PROTECTED_USER_FIELDS', [])) + tuple(strategy.setting('PROTECTED_USER_FIELDS', [])) # Update user model attributes with the new data sent by the current # provider. Update on some attributes is disabled by default, for From c465257e3f64bbdab0c3f9d8e4f2eb71d463d071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 16 Apr 2015 20:46:50 -0300 Subject: [PATCH 562/890] Fix clean username regex. Fixes #594 --- social/storage/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/storage/base.py b/social/storage/base.py index fe246e940..475b70cf2 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -15,7 +15,7 @@ from social.strategies.utils import get_current_strategy -CLEAN_USERNAME_REGEX = re.compile(r'[^\w.@+-_]+', re.UNICODE) +CLEAN_USERNAME_REGEX = re.compile(r'[^\w.@+_-]+', re.UNICODE) class UserMixin(object): From acbbf93e2177e5e0b252035355f9495e8ed6c4b3 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Fri, 17 Apr 2015 15:40:26 -0700 Subject: [PATCH 563/890] ChangeTip backend --- social/backends/changetip.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 social/backends/changetip.py diff --git a/social/backends/changetip.py b/social/backends/changetip.py new file mode 100644 index 000000000..75e043da6 --- /dev/null +++ b/social/backends/changetip.py @@ -0,0 +1,29 @@ +from social.backends.oauth import BaseOAuth2 +from urllib import urlencode, urlopen +import json + + +class ChangeTipOAuth2(BaseOAuth2): + """ChangeTip OAuth authentication backend + https://www.changetip.com/api + """ + name = 'changetip' + AUTHORIZATION_URL = 'https://www.changetip.com/o/authorize/' + ACCESS_TOKEN_URL = 'https://www.changetip.com/o/token/' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ' ' + + def get_user_details(self, response): + """Return user details from ChangeTip account""" + return {'username': response['username'], + 'email': response['email'] or ''} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + url = 'https://www.changetip.com/v2/me/?' + urlencode({ + 'access_token': access_token + }) + try: + return json.load(urlopen(url)) + except ValueError: + return None From 83ba360fde9d889502b3887f623be266f6aab4b1 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Fri, 17 Apr 2015 16:32:14 -0700 Subject: [PATCH 564/890] use api domain for changetip request --- social/backends/changetip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/changetip.py b/social/backends/changetip.py index 75e043da6..10470d50f 100644 --- a/social/backends/changetip.py +++ b/social/backends/changetip.py @@ -20,7 +20,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - url = 'https://www.changetip.com/v2/me/?' + urlencode({ + url = 'https://api.changetip.com/v2/me/?' + urlencode({ 'access_token': access_token }) try: From 7476575ec92a3a50b799727b0db07d009279c73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 19 Apr 2015 04:11:44 -0300 Subject: [PATCH 565/890] Don't send redirect_state to slack backend --- social/backends/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/slack.py b/social/backends/slack.py index f1e439be0..74775c8fc 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -15,7 +15,7 @@ class SlackOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://slack.com/api/oauth.access' ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' - REDIRECT_STATE = True + REDIRECT_STATE = False EXTRA_DATA = [ ('id', 'id'), ('name', 'name'), From 789e0dc93017f6956f95c2f07260136e5d20102b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 19 Apr 2015 04:13:32 -0300 Subject: [PATCH 566/890] v0.2.7 --- Changelog | 43 +++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index f93ad700d..1b68e6973 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,49 @@ +2015-04-19 v0.2.7 +================= + + * 2015-04-19 Matías Aguirre + Don't send redirect_state to slack backend + + * 2015-04-16 Matías Aguirre + Fix clean username regex. Fixes #594 + + * 2015-04-16 Matías Aguirre + PEP8 and switch check order + + * 2015-04-16 Matías Aguirre + Take into account that sometimes API v2.3 returns the old querystring + format. Fixes #592 + + * 2015-04-16 Matías Aguirre + Move OAuth1 method out from the base class + + * 2015-04-16 zz + Fix the final_username may be empty and will skip the loop. + + * 2015-04-16 ys.chi + Alter email max length for Django app + + * 2015-04-15 Matías Aguirre + Remove single-use var + + * 2015-04-15 Matías Aguirre + Clean any pipeline remanents when starting the process. Refs #325 + + * 2015-04-15 Matías Aguirre + Flag dev version + + * 2015-04-15 Matías Aguirre + Swtich Twitter API to POST (as it's documented) + + * 2015-04-15 Christian Pedersen + Append trailing slash in Django + 2015-04-14 v0.2.6 ================= + * 2015-04-14 Matías Aguirre + v0.2.6 + * 2015-04-14 Matías Aguirre Include tests requirements files. Fixes #590 diff --git a/social/__init__.py b/social/__init__.py index 49066c214..2cd65751f 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 7) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From b5d62ee7f7402adfb8b96e3be8ca980c5f16b8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 19 Apr 2015 04:16:05 -0300 Subject: [PATCH 567/890] Flag dev version --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 2cd65751f..b8031759e 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 7) -extra = '' +version = (0, 2, 8) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From 3ae0c281f0e2aabaceea0b85f20fa75bd5c0b42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 19 Apr 2015 04:32:05 -0300 Subject: [PATCH 568/890] Allow to remove team from username in slack backend --- social/backends/slack.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/social/backends/slack.py b/social/backends/slack.py index 74775c8fc..ac6063487 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -26,8 +26,10 @@ def get_user_details(self, response): """Return user details from Slack account""" # Build the username with the team $username@$team_url # Necessary to get unique names for all of slack - match = re.search(r'//([^.]+)\.slack\.com', response['url']) - username = '{0}@{1}'.format(response.get("user"), match.group(1)) + username = response.get('user') + if self.setting('USERNAME_WITH_TEAM', True): + match = re.search(r'//([^.]+)\.slack\.com', response['url']) + username = '{0}@{1}'.format(username, match.group(1)) out = {'username': username} if 'profile' in response: From 85a79ac592f035ca18d04b1beca02604e544dc51 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 20 Apr 2015 10:17:41 -0700 Subject: [PATCH 569/890] fail gracefully on missing email --- social/backends/changetip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/changetip.py b/social/backends/changetip.py index 10470d50f..d2e49c3d5 100644 --- a/social/backends/changetip.py +++ b/social/backends/changetip.py @@ -16,7 +16,7 @@ class ChangeTipOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from ChangeTip account""" return {'username': response['username'], - 'email': response['email'] or ''} + 'email': response.get('email', '')} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" From ca017368ddaf0154970a87b8fae07e0cab7874e4 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 20 Apr 2015 11:22:33 -0700 Subject: [PATCH 570/890] Documentation for ChangeTip backend --- docs/backends/changetip.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/backends/changetip.rst diff --git a/docs/backends/changetip.rst b/docs/backends/changetip.rst new file mode 100644 index 000000000..dcc9cc502 --- /dev/null +++ b/docs/backends/changetip.rst @@ -0,0 +1,22 @@ +ChangeTip +===== + +ChangeTip + +- Register a new application at ChangeTip_, set the callback URL to + ``http://example.com/complete/changetip/`` replacing ``example.com`` with your + domain. + +- Fill ``Client ID`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_CHANGETIP_KEY = '' + SOCIAL_AUTH_CHANGETIP_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_CHANGETIP_SCOPE = [...] + + See auth scopes at `ChangeTip OAuth docs`_. + + +.. _ChangeTip: https://www.changetip.com/api From e3f2f22d622fd5f8490c7973ad3c4abce83dd91a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 20 Apr 2015 16:39:27 -0300 Subject: [PATCH 571/890] Use get_json() helper --- social/backends/changetip.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/social/backends/changetip.py b/social/backends/changetip.py index d2e49c3d5..dfb8206bf 100644 --- a/social/backends/changetip.py +++ b/social/backends/changetip.py @@ -1,6 +1,4 @@ from social.backends.oauth import BaseOAuth2 -from urllib import urlencode, urlopen -import json class ChangeTipOAuth2(BaseOAuth2): @@ -15,15 +13,15 @@ class ChangeTipOAuth2(BaseOAuth2): def get_user_details(self, response): """Return user details from ChangeTip account""" - return {'username': response['username'], - 'email': response.get('email', '')} + return { + 'username': response['username'], + 'email': response.get('email', ''), + 'first_name': '', + 'last_name': '', + } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - url = 'https://api.changetip.com/v2/me/?' + urlencode({ + return self.get_json('https://api.changetip.com/v2/me/', params={ 'access_token': access_token }) - try: - return json.load(urlopen(url)) - except ValueError: - return None From 1851b5faeaa84a6544f584719104c0b2534e79a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 21 Apr 2015 20:29:31 -0300 Subject: [PATCH 572/890] Make URLs trailing slash be configurable by setting. Refs #505 --- social/apps/django_app/urls.py | 17 ++++++++++++----- social/apps/django_app/utils.py | 3 --- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/social/apps/django_app/urls.py b/social/apps/django_app/urls.py index add764c2d..43a97aa8f 100644 --- a/social/apps/django_app/urls.py +++ b/social/apps/django_app/urls.py @@ -1,4 +1,5 @@ """URLs module""" +from django.conf import settings try: from django.conf.urls import patterns, url except ImportError: @@ -6,15 +7,21 @@ from django.conf.urls.defaults import patterns, url +from social.utils import setting_name + + +extra = getattr(settings, setting_name('TRAILING_SLASH'), True) and '/' or '' + + urlpatterns = patterns('social.apps.django_app.views', # authentication / association - url(r'^login/(?P[^/]+)/?$', 'auth', + url(r'^login/(?P[^/]+){0}$'.format(extra), 'auth', name='begin'), - url(r'^complete/(?P[^/]+)/?$', 'complete', + url(r'^complete/(?P[^/]+){0}$'.format(extra), 'complete', name='complete'), # disconnection - url(r'^disconnect/(?P[^/]+)/?$', 'disconnect', + url(r'^disconnect/(?P[^/]+){0}$'.format(extra), 'disconnect', name='disconnect'), - url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/?$', - 'disconnect', name='disconnect_individual'), + url(r'^disconnect/(?P[^/]+)/(?P[^/]+){0}$' + .format(extra), 'disconnect', name='disconnect_individual'), ) diff --git a/social/apps/django_app/utils.py b/social/apps/django_app/utils.py index ae20098c7..b4a50ec5e 100644 --- a/social/apps/django_app/utils.py +++ b/social/apps/django_app/utils.py @@ -37,9 +37,6 @@ def wrapper(request, backend, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith('/'): uri = reverse(redirect_uri, args=(backend,)) - if settings.APPEND_SLASH and not uri.endswith('/'): - uri = uri + '/' - request.social_strategy = load_strategy(request) # backward compatibility in attribute name, only if not already # defined From f88b9c5493fe6356f44b1bb89a6697e21f9b1d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 27 Apr 2015 11:34:46 -0300 Subject: [PATCH 573/890] Fix typos in docs (thanks to vsobolmaven) --- docs/index.rst | 2 +- docs/intro.rst | 2 +- docs/storage.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2829f7b01..1f91df172 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ The initial codebase is derived from django-social-auth_ with the idea of generalizing the process to suite the different frameworks around, providing the needed tools to bring support to new frameworks. -django-social-auth_ itself was a product of modified code from from +django-social-auth_ itself was a product of modified code from django-twitter-oauth_ and django-openid-auth_ projects. diff --git a/docs/intro.rst b/docs/intro.rst index b835961f4..b138974dc 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -93,7 +93,7 @@ or extend current one): User data ********* -Basic user data population, to allows custom fields values from providers +Basic user data population, to allow custom fields values from providers response. diff --git a/docs/storage.rst b/docs/storage.rst index 80e177d98..e12e13bce 100644 --- a/docs/storage.rst +++ b/docs/storage.rst @@ -158,7 +158,7 @@ storage modules. When implementing this class it must inherits from BaseStorage_, add the needed models references and implement the needed method:: - class StorageImlpementation(BaseStorage): + class StorageImplementation(BaseStorage): user = UserModel nonce = NonceModel association = AssociationModel From 7ff533bce988a2753227c1aa04c31a04d9570bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 1 May 2015 01:08:48 -0300 Subject: [PATCH 574/890] Support SSL protocol override, default Amazon to TLSv1. Fixes #603 --- social/backends/amazon.py | 3 +++ social/backends/base.py | 10 +++++++--- social/utils.py | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/social/backends/amazon.py b/social/backends/amazon.py index c97b8a09f..247574454 100644 --- a/social/backends/amazon.py +++ b/social/backends/amazon.py @@ -2,6 +2,8 @@ Amazon OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/amazon.html """ +import ssl + from social.backends.oauth import BaseOAuth2 @@ -13,6 +15,7 @@ class AmazonOAuth2(BaseOAuth2): DEFAULT_SCOPE = ['profile'] REDIRECT_STATE = False ACCESS_TOKEN_METHOD = 'POST' + SSL_PROTOCOL = ssl.PROTOCOL_TLSv1 EXTRA_DATA = [ ('refresh_token', 'refresh_token', True), ('user_id', 'user_id'), diff --git a/social/backends/base.py b/social/backends/base.py index fcb910823..f55e8e377 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -1,6 +1,6 @@ from requests import request, ConnectionError -from social.utils import module_member, parse_qs, user_agent +from social.utils import SSLHttpAdapter, module_member, parse_qs, user_agent from social.exceptions import AuthFailed @@ -13,6 +13,7 @@ class BaseAuth(object): EXTRA_DATA = None REQUIRES_EMAIL_VALIDATION = False SEND_USER_AGENT = False + SSL_PROTOCOL = None def __init__(self, strategy=None, redirect_uri=None): self.strategy = strategy @@ -210,12 +211,15 @@ def request(self, url, method='GET', *args, **kwargs): kwargs.setdefault('verify', self.setting('VERIFY_SSL')) kwargs.setdefault('timeout', self.setting('REQUESTS_TIMEOUT') or self.setting('URLOPEN_TIMEOUT')) - if self.SEND_USER_AGENT and 'User-Agent' not in kwargs['headers']: kwargs['headers']['User-Agent'] = user_agent() try: - response = request(method, url, *args, **kwargs) + if self.SSL_PROTOCOL: + session = SSLHttpAdapter.ssl_adapter_session(self.SSL_PROTOCOL) + response = session.request(method, url, *args, **kwargs) + else: + response = request(method, url, *args, **kwargs) except ConnectionError as err: raise AuthFailed(self, str(err)) response.raise_for_status() diff --git a/social/utils.py b/social/utils.py index 27982a91a..2cd41df43 100644 --- a/social/utils.py +++ b/social/utils.py @@ -9,6 +9,9 @@ import requests import social +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.poolmanager import PoolManager + from social.exceptions import AuthCanceled, AuthUnreachableProvider from social.p3 import urlparse, urlunparse, urlencode, \ parse_qs as battery_parse_qs @@ -19,6 +22,30 @@ social_logger = logging.Logger('social') +class SSLHttpAdapter(HTTPAdapter): + """" + Transport adapter that allows to use any SSL protocol. Based on: + http://requests.rtfd.org/latest/user/advanced/#example-specific-ssl-version + """ + def __init__(self, ssl_protocol): + self.ssl_protocol = ssl_protocol + super(SSLHttpAdapter, self).__init__() + + def init_poolmanager(self, connections, maxsize, block=False): + self.poolmanager = PoolManager( + num_pools=connections, + maxsize=maxsize, + block=block, + ssl_version=self.ssl_protocol + ) + + @classmethod + def ssl_adapter_session(cls, ssl_protocol): + session = requests.Session() + session.mount('https://', SSLHttpAdapter(ssl_protocol)) + return session + + def import_module(name): __import__(name) return sys.modules[name] From eb1017447ba180c131f535bcc26e5e9e4cef0a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 1 May 2015 01:10:27 -0300 Subject: [PATCH 575/890] Add note about TLSv1 support in Amazon backend --- docs/backends/amazon.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/backends/amazon.rst b/docs/backends/amazon.rst index 7c2396ff1..72ccd0a95 100644 --- a/docs/backends/amazon.rst +++ b/docs/backends/amazon.rst @@ -21,6 +21,9 @@ enable ``python-social-auth`` support follow this steps: Further documentation at `Website Developer Guide`_ and `Getting Started for Web`_. +**Note:** This backend supports TLSv1 protocol since SSL will be deprecated + from May 25, 2015 + .. _Amazon App Console: http://login.amazon.com/manageApps .. _Website Developer Guide: https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf .. _Getting Started for Web: http://login.amazon.com/website From 0eb735df32397c50d7232640687f625fbfb21e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Avi=20=D7=90=D7=91=D7=99=20Alkalay=20=D7=90=D7=9C=D7=A7?= =?UTF-8?q?=D7=9C=D7=A2=D7=99?= Date: Sat, 2 May 2015 05:46:07 -0300 Subject: [PATCH 576/890] Added Moves to the list of providers --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 5817f6733..ab03d2d07 100644 --- a/README.rst +++ b/README.rst @@ -89,6 +89,7 @@ or current ones extended): * MapMyFitness_ OAuth2 * Mendeley_ OAuth1 http://mendeley.com * Mixcloud_ OAuth2 + * Moves_ app OAuth2 https://dev.moves-app.com/docs/authentication * `Mozilla Persona`_ * NaszaKlasa_ OAuth2 * Odnoklassniki_ OAuth2 and Application Auth From af7df798e5065826b806566d861bd9e202e54f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Avi=20=D7=90=D7=91=D7=99=20Alkalay=20=D7=90=D7=9C=D7=A7?= =?UTF-8?q?=D7=9C=D7=A2=D7=99?= Date: Sat, 2 May 2015 05:51:10 -0300 Subject: [PATCH 577/890] Fixed Moves link on list of providers --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ab03d2d07..73f6cf715 100644 --- a/README.rst +++ b/README.rst @@ -89,7 +89,7 @@ or current ones extended): * MapMyFitness_ OAuth2 * Mendeley_ OAuth1 http://mendeley.com * Mixcloud_ OAuth2 - * Moves_ app OAuth2 https://dev.moves-app.com/docs/authentication + * `Moves app`_ OAuth2 https://dev.moves-app.com/docs/authentication * `Mozilla Persona`_ * NaszaKlasa_ OAuth2 * Odnoklassniki_ OAuth2 and Application Auth @@ -260,6 +260,7 @@ check `django-social-auth LICENSE`_ for details: .. _Mailru: https://mail.ru .. _MapMyFitness: http://www.mapmyfitness.com/ .. _Mixcloud: https://www.mixcloud.com +.. _Moves app: https://dev.moves-app.com/docs/ .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _NaszaKlasa: https://developers.nk.pl/ .. _Odnoklassniki: http://www.odnoklassniki.ru From a68f018eb0828db2c97e8f0a62787a32838e7e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 7 May 2015 15:10:35 -0300 Subject: [PATCH 578/890] v0.2.8 --- Changelog | 45 +++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 1b68e6973..a38dbcd38 100644 --- a/Changelog +++ b/Changelog @@ -1,9 +1,54 @@ +2015-05-02 v0.2.8 +================= + + * 2015-05-02 Avi אבי Alkalay אלקלעי + Fixed Moves link on list of providers + + * 2015-05-02 Avi אבי Alkalay אלקלעי + Added Moves to the list of providers + + * 2015-05-01 Matías Aguirre + Add note about TLSv1 support in Amazon backend + + * 2015-05-01 Matías Aguirre + Support SSL protocol override, default Amazon to TLSv1. Fixes #603 + + * 2015-04-27 Matías Aguirre + Fix typos in docs (thanks to vsobolmaven) + + * 2015-04-21 Matías Aguirre + Make URLs trailing slash be configurable by setting. Refs #505 + + * 2015-04-20 Matías Aguirre + Use get_json() helper + + * 2015-04-20 Nick Sullivan + Documentation for ChangeTip backend + + * 2015-04-20 Nick Sullivan + fail gracefully on missing email + + * 2015-04-19 Matías Aguirre + Allow to remove team from username in slack backend + + * 2015-04-19 Matías Aguirre + Flag dev version + 2015-04-19 v0.2.7 ================= + * 2015-04-19 Matías Aguirre + v0.2.7 + * 2015-04-19 Matías Aguirre Don't send redirect_state to slack backend + * 2015-04-17 Nick Sullivan + use api domain for changetip request + + * 2015-04-17 Nick Sullivan + ChangeTip backend + * 2015-04-16 Matías Aguirre Fix clean username regex. Fixes #594 diff --git a/social/__init__.py b/social/__init__.py index b8031759e..055ddd32e 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 8) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 1df1de54092b1cd5e85887377951be1da6afdd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 7 May 2015 15:21:53 -0300 Subject: [PATCH 579/890] Fix manifest definition --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 7675886d6..2cf5b4839 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,8 @@ recursive-include social/tests *.txt graft examples +recursive-exclude .tox * +recursive-exclude python_social_auth.egg-info * recursive-exclude social *.pyc recursive-exclude examples *.pyc recursive-exclude examples *.db From 8f9be44e8b49c430b1d012d0e4c26160e714afa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 7 May 2015 15:22:47 -0300 Subject: [PATCH 580/890] v0.2.9 --- Changelog | 11 ++++++++++- social/__init__.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index a38dbcd38..cf8a91e01 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,15 @@ -2015-05-02 v0.2.8 +2015-05-07 v0.2.9 ================= + * 2015-05-07 Matías Aguirre + Fix manifest definition + +2015-05-07 v0.2.8 +================= + + * 2015-05-07 Matías Aguirre + v0.2.8 + * 2015-05-02 Avi אבי Alkalay אלקלעי Fixed Moves link on list of providers diff --git a/social/__init__.py b/social/__init__.py index 055ddd32e..13abfd0a0 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 8) +version = (0, 2, 9) extra = '' __version__ = '.'.join(map(str, version)) + extra From d6cedb5c242ccc0725fbf7886d7a8f1f99334cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 7 May 2015 15:24:32 -0300 Subject: [PATCH 581/890] Dev version flagged --- social/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/__init__.py b/social/__init__.py index 13abfd0a0..365327065 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 9) -extra = '' +version = (0, 2, 10) +extra = '-dev' __version__ = '.'.join(map(str, version)) + extra From e6357827a670c71e2489b5468b89a65153719ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 8 May 2015 01:46:06 -0300 Subject: [PATCH 582/890] Fix syntax (backward compatible) --- social/strategies/tornado_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index 0f2a525ec..7e3f11252 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -28,7 +28,8 @@ def get_setting(self, name): def request_data(self, merge=True): # Multiple valued arguments not supported yet - return {key: val[0] for key, val in self.request.arguments.iteritems()} + return dict((key, val[0]) + for key, val in self.request.arguments.iteritems()) def request_host(self): return self.request.host From 4d4c3f8e1b1023f61b87751ba448bb948b51e912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 8 May 2015 01:51:01 -0300 Subject: [PATCH 583/890] Ensure that all the requirements are installed --- .travis.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cfb9e6402..f7aca6f7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python env: global: + - REQUIREMENTS=requirements.txt - TEST_REQUIREMENTS=social/tests/requirements.txt python: - "2.6" @@ -9,11 +10,16 @@ python: matrix: include: - python: "3.3" - env: TEST_REQUIREMENTS=social/tests/requirements-python3.txt + env: + - REQUIREMENTS=requirements-python3.txt + - TEST_REQUIREMENTS=social/tests/requirements-python3.txt - python: "3.4" - env: TEST_REQUIREMENTS=social/tests/requirements-python3.txt + env: + - REQUIREMENTS=requirements-python3.txt + - TEST_REQUIREMENTS=social/tests/requirements-python3.txt install: - "python setup.py -q install" + - "travis_retry pip install -r $REQUIREMENTS" - "travis_retry pip install -r $TEST_REQUIREMENTS" script: - "nosetests --with-coverage --cover-package=social --where=social/tests" From 9fe88e6fe7d968beb5940b886792c8114ccc7e50 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Thu, 30 Apr 2015 17:42:08 -0700 Subject: [PATCH 584/890] SAML2 backend using OneLogin's python-saml --- social/backends/saml.py | 277 +++++++++++++++++++++++++++ social/strategies/base.py | 20 ++ social/strategies/django_strategy.py | 20 ++ 3 files changed, 317 insertions(+) create mode 100644 social/backends/saml.py diff --git a/social/backends/saml.py b/social/backends/saml.py new file mode 100644 index 000000000..135cf4086 --- /dev/null +++ b/social/backends/saml.py @@ -0,0 +1,277 @@ +""" +Backend for SAML 2.0 support + +Terminology: + +"Service Provider" (SP): Your web app +"Identity Provider" (IdP): The third-party site that is authenticating users via SAML +""" +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from social.backends.base import BaseAuth +from social.exceptions import AuthFailed + +# Helpful constants: +OID_COMMON_NAME = "urn:oid:2.5.4.3" +OID_EDU_PERSON_PRINCIPAL_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" +OID_GIVEN_NAME = "urn:oid:2.5.4.42" +OID_MAIL = "urn:oid:0.9.2342.19200300.100.1.3" +OID_SURNAME = "urn:oid:2.5.4.4" +OID_USERID = "urn:oid:0.9.2342.19200300.100.1.1" + + +class SAMLIdentityProvider(object): + """ + Wrapper around configuration for a SAML Identity provider + """ + + def __init__(self, name, **kwargs): + """ Load and parse configuration """ + self.name = name + assert self.name.isalnum() # If 'name' contained a colon, it would affect our UID mangling + self.conf = kwargs + + def get_user_permanent_id(self, attributes): + """ + The most important method: Get a permanent, unique identifier for this user from the + attributes supplied by the IdP. + + If you want to use the NameID, it's available via attributes['name_id'] + """ + return attributes[self.conf.get('user_permanent_id', OID_USERID)][0] + + # Attributes processing: + def get_user_details(self, attributes): + """ + Given the SAML attributes extracted from the SSO response, get the user data like name. + """ + return { + 'fullname': self.get_attr(attributes, 'attr_full_name', OID_COMMON_NAME), + 'first_name': self.get_attr(attributes, 'attr_first_name', OID_GIVEN_NAME), + 'last_name': self.get_attr(attributes, 'attr_last_name', OID_SURNAME), + 'username': self.get_attr(attributes, 'attr_username', OID_USERID), + 'email': self.get_attr(attributes, 'attr_email', OID_MAIL), + } + + def get_attr(self, attributes, conf_key, default_attribute): + """ + Internal helper method. + Get the attribute 'default_attribute' out of the attributes, unless self.conf[conf_key] + overrides the default by specifying another attribute to use. + """ + key = self.conf.get(conf_key, default_attribute) + return attributes[key][0] if key in attributes else None + + @property + def entity_id(self): + """ Get the entity ID for this IdP """ + return self.conf['entity_id'] # Required. e.g. "https://idp.testshib.org/idp/shibboleth" + + @property + def sso_url(self): + """ Get the SSO URL for this IdP """ + return self.conf['url'] # Required. e.g. "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" + + @property + def sso_binding(self): + """ Get the method used to submit our request to the SSO URL """ + return self.conf.get('binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect') + + @property + def x509cert(self): + """ X.509 Public Key Certificate for this IdP """ + return self.conf['x509cert'] + + @property + def saml_config_dict(self): + """ Get the IdP configuration dict in the format required by python-saml """ + return { + "entityId": self.entity_id, + "singleSignOnService": { + "url": self.sso_url, + "binding": self.sso_binding, + }, + "x509cert": self.x509cert, + } + + +class DummySAMLIdentityProvider(SAMLIdentityProvider): + """ + A placeholder IdP used when we must specify something, e.g. when generating SP metadata. + + If OneLogin_Saml2_Auth is modified to not always require IdP config, this can be removed. + """ + def __init__(self): + super(DummySAMLIdentityProvider, self).__init__( + "dummy", + entity_id="https://dummy.none/saml2", + url="https://dummy.none/SSO", + x509cert='', + ) + + +class SAMLAuth(BaseAuth): + """ + PSA Backend that implements SAML 2.0 Service Provider (SP) functionality. + + Unlike all of the other backends, this one can be configured to work with + many identity providers (IdPs). For example, a University that belongs to a + Shibboleth federation may support authentication via ~100 partner + universities. Also, the IdP configuration can be changed at runtime if you + require that functionality - just subclass this and override `get_idp()`. + + Several settings are required. Here's an example: + + SOCIAL_AUTH_SAML_SP_ENTITY_ID = "https://saml.example.com/" + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = "... X.509 certificate string ..." + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = "... private key ..." + SOCIAL_AUTH_SAML_ORG_INFO = { + "en-US": {"name": "example", "displayname": "Example Inc.", "url": "http://example.com", }, + } + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {"givenName": "Tech Gal", "emailAddress": "technical@example.com", } + SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {"givenName": "Support Guy", "emailAddress": "support@example.com", } + SOCIAL_AUTH_SAML_ENABLED_IDPS = { + "testshib": { + "entity_id": "https://idp.testshib.org/idp/shibboleth", + "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", + "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0B ... 8Bbnl+ev0peYzxFyF5sQA==", + } + } + + Optional settings: + SOCIAL_AUTH_SAML_SP_EXTRA = {} + SOCIAL_AUTH_SAML_SECURITY_CONFIG = {} + SOCIAL_AUTH_SAML_SP_NAMEID_FORMATS = [] + """ + name = "saml" + + def get_idp(self, idp_name): + """ Given the name of an IdP, get a SAMLIdentityProvider instance """ + idp_config = self.setting("ENABLED_IDPS")[idp_name] + return SAMLIdentityProvider(idp_name, **idp_config) + + def generate_saml_config(self, idp): + """ + Generate the configuration required to instantiate OneLogin_Saml2_Auth + """ + # The shared absolute URL that all IdPs redirect back to - this is specified in our metadata.xml: + abs_completion_url = self.redirect_uri + + config = { + "contactPerson": { + "technical": self.setting("TECHNICAL_CONTACT"), + "support": self.setting("SUPPORT_CONTACT"), + }, + "debug": True, + "idp": idp.saml_config_dict, + "organization": self.setting("ORG_INFO"), + "security": { + 'metadataValidUntil': '', + 'metadataCacheDuration': 'P10D', # metadata valid for ten days + }, + "sp": { + "assertionConsumerService": { + "url": abs_completion_url, + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + }, + "entityId": self.setting("SP_ENTITY_ID"), + "NameIDFormats": self.setting("SP_NAMEID_FORMATS", []), + "x509cert": self.setting("SP_PUBLIC_CERT"), + "privateKey": self.setting("SP_PRIVATE_KEY"), + }, + "strict": True, # We must force strict mode - for security + } + config["security"].update(self.setting("SECURITY_CONFIG", {})) + config["sp"].update(self.setting("SP_EXTRA", {})) + return config + + def generate_metadata_xml(self): + """ + Helper method that can be used from your web app to generate the XML metadata required + to link your web app as a Service Provider with each IdP you wish to use. + + Returns (metadata XML string, list of errors) + + Example usage (Django): + from social.apps.django_app.utils import load_strategy, load_backend + def saml_metadata_view(request): + complete_url = reverse('social:complete', args=("saml", )) + saml_backend = load_backend(load_strategy(request), "saml", complete_url) + metadata, errors = saml_backend.generate_metadata_xml() + if not errors: + return HttpResponse(content=metadata, content_type='text/xml') + return HttpResponseServerError(content=', '.join(errors)) + """ + idp = DummySAMLIdentityProvider() # python-saml requires us to specify something here even though it's not used + config = self.generate_saml_config(idp) + saml_settings = OneLogin_Saml2_Settings(config) + metadata = saml_settings.get_sp_metadata() + errors = saml_settings.validate_metadata(metadata) + return metadata, errors + + def _create_saml_auth(self, idp_name): + """ + Get an instance of OneLogin_Saml2_Auth + """ + config = self.generate_saml_config(idp=self.get_idp(idp_name)) + request_info = { + 'https': 'on' if self.strategy.request_is_secure() else 'off', + 'http_host': self.strategy.request_host(), + 'script_name': self.strategy.request_path(), + 'server_port': self.strategy.request_port(), + 'get_data': self.strategy.request_get(), + 'post_data': self.strategy.request_post(), + } + return OneLogin_Saml2_Auth(request_info, config) + + def auth_url(self): + """ Get the URL to which we must redirect in order to authenticate the user """ + idp_name = self.strategy.request_data()['idp'] + auth = self._create_saml_auth(idp_name) + # Below, return_to sets the RelayState, which can contain arbitrary data. + # We use it to store the specific SAML IdP backend name, since we combine + # many backends to a single URL. + return auth.login(return_to=idp_name) + + def get_user_details(self, response): + """ + Get user details like full name, email, etc. from the response - see auth_complete + """ + idp = self.get_idp(response['idp_name']) + return idp.get_user_details(response['attributes']) + + def get_user_id(self, details, response): + """ + Get the permanent ID for this user from the response. + We prefix each ID with the name of the IdP so that we can connect multiple IdPs to this + user. + """ + idp = self.get_idp(response['idp_name']) + uid = idp.get_user_permanent_id(response['attributes']) + return '{}:{}'.format(idp.name, uid) + + def auth_complete(self, *args, **kwargs): + """ + The user has been redirected back from the IdP and we should now log them in, if + everything checks out. + """ + idp_name = self.strategy.request_data()['RelayState'] + auth = self._create_saml_auth(idp_name) + auth.process_response() + errors = auth.get_errors() + if errors or not auth.is_authenticated(): + reason = auth.get_last_error_reason() + raise AuthFailed(self, 'SAML login failed: {} ({})'.format(errors, reason)) + + attributes = auth.get_attributes() + attributes['name_id'] = auth.get_nameid() + + response = { + 'idp_name': idp_name, + 'attributes': attributes, + 'session_index': auth.get_session_index(), + } + + kwargs.update({'response': response, 'backend': self}) + + return self.strategy.authenticate(*args, **kwargs) diff --git a/social/strategies/base.py b/social/strategies/base.py index f2273b972..09ef9fa27 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -188,3 +188,23 @@ def session_pop(self, name): def build_absolute_uri(self, path=None): """Build absolute URI with given (optional) path""" raise NotImplementedError('Implement in subclass') + + def request_is_secure(self): + """ Is the request using HTTPS? """ + raise NotImplementedError('Implement in subclass') + + def request_path(self): + """ path of the current request """ + raise NotImplementedError('Implement in subclass') + + def request_port(self): + """ Port in use for this request """ + raise NotImplementedError('Implement in subclass') + + def request_get(self): + """ Request GET data """ + raise NotImplementedError('Implement in subclass') + + def request_post(self): + """ Request POST data """ + raise NotImplementedError('Implement in subclass') diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 7e80f03fa..b3b66b791 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -53,6 +53,26 @@ def request_host(self): if self.request: return self.request.get_host() + def request_is_secure(self): + """ Is the request using HTTPS? """ + return self.request.is_secure() + + def request_path(self): + """ path of the current request """ + return self.request.path + + def request_port(self): + """ Port in use for this request """ + return self.request.META['SERVER_PORT'] + + def request_get(self): + """ Request GET data """ + return self.request.GET.copy() + + def request_post(self): + """ Request POST data """ + return self.request.POST.copy() + def redirect(self, url): return redirect(url) From 05ca867f1166fdb63b9fb838a0b8e44b7d48d11d Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Fri, 8 May 2015 00:47:07 -0700 Subject: [PATCH 585/890] Tests for SAML backend --- .travis.yml | 3 + social/tests/actions/test_disconnect.py | 13 ++- social/tests/backends/data/saml_config.json | 26 +++++ social/tests/backends/data/saml_response.txt | 1 + social/tests/backends/test_saml.py | 104 +++++++++++++++++++ social/tests/models.py | 2 +- social/tests/strategy.py | 20 ++++ 7 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 social/tests/backends/data/saml_config.json create mode 100644 social/tests/backends/data/saml_response.txt create mode 100644 social/tests/backends/test_saml.py diff --git a/.travis.yml b/.travis.yml index f7aca6f7e..d9380b2ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,9 @@ matrix: env: - REQUIREMENTS=requirements-python3.txt - TEST_REQUIREMENTS=social/tests/requirements-python3.txt +before_install: + - sudo apt-get update -qq + - sudo apt-get install -y libxmlsec1-dev swig install: - "python setup.py -q install" - "travis_retry pip install -r $REQUIREMENTS" diff --git a/social/tests/actions/test_disconnect.py b/social/tests/actions/test_disconnect.py index 328ad8d0c..ef89d5939 100644 --- a/social/tests/actions/test_disconnect.py +++ b/social/tests/actions/test_disconnect.py @@ -6,7 +6,7 @@ from social.exceptions import NotAllowedToDisconnect from social.utils import parse_qs -from social.tests.models import User +from social.tests.models import User, TestUserSocialAuth from social.tests.actions.actions import BaseActionTest @@ -24,6 +24,17 @@ def test_disconnect(self): do_disconnect(self.backend, user) self.assertEqual(len(user.social), 0) + def test_disconnect_with_association_id(self): + self.do_login() + user = User.get(self.expected_username) + user.password = 'password' + association_id = user.social[0].id + second_usa = TestUserSocialAuth(user, user.social[0].provider, "uid2") + self.assertEqual(len(user.social), 2) + do_disconnect(self.backend, user, association_id) + self.assertEqual(len(user.social), 1) + self.assertEqual(user.social[0], second_usa) + def test_disconnect_with_partial_pipeline(self): self.strategy.set_settings({ 'SOCIAL_AUTH_DISCONNECT_PIPELINE': ( diff --git a/social/tests/backends/data/saml_config.json b/social/tests/backends/data/saml_config.json new file mode 100644 index 000000000..5c119e6a7 --- /dev/null +++ b/social/tests/backends/data/saml_config.json @@ -0,0 +1,26 @@ +{ + "SOCIAL_AUTH_SAML_SP_ENTITY_ID": "https://github.com/omab/python-social-auth/saml-test", + "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "MIICsDCCAhmgAwIBAgIJAO7BwdjDZcUWMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNBMRkwFwYDVQQIExBCcml0aXNoIENvbHVtYmlhMRswGQYDVQQKExJweXRob24tc29jaWFsLWF1dGgwHhcNMTUwNTA4MDc1ODQ2WhcNMjUwNTA3MDc1ODQ2WjBFMQswCQYDVQQGEwJDQTEZMBcGA1UECBMQQnJpdGlzaCBDb2x1bWJpYTEbMBkGA1UEChMScHl0aG9uLXNvY2lhbC1hdXRoMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCq3g1Cl+3uR5vCnN4HbgjTg+m3nHhteEMyb++ycZYre2bxUfsshER6x33l23tHckRYwm7MdBbrp3LrVoiOCdPblTml1IhEPTCwKMhBKvvWqTvgfcSSnRzAWkLlQYSusayyZK4n9qcYkV5MFni1rbjx+Mr5aOEmb5u33amMKLwSTwIDAQABo4GnMIGkMB0GA1UdDgQWBBRRiBR6zS66fKVokp0yJHbgv3RYmjB1BgNVHSMEbjBsgBRRiBR6zS66fKVokp0yJHbgv3RYmqFJpEcwRTELMAkGA1UEBhMCQ0ExGTAXBgNVBAgTEEJyaXRpc2ggQ29sdW1iaWExGzAZBgNVBAoTEnB5dGhvbi1zb2NpYWwtYXV0aIIJAO7BwdjDZcUWMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJwsMU3YSaybVjuJ8US0fUhlPOlM40QFCGL4vB3TEbb24Mq8HrjUwrU0JFPGls9a2OYzN2B3e35NorMuxs+grGtr2yP6LvuX+nV6A93wb4ooGHoGfC7VLlyxSSns937SS5R1pzQ4gWzZma2KGWKICWph5zQ0ARVhL63967mGLmoI=", + "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "MIICXgIBAAKBgQCq3g1Cl+3uR5vCnN4HbgjTg+m3nHhteEMyb++ycZYre2bxUfsshER6x33l23tHckRYwm7MdBbrp3LrVoiOCdPblTml1IhEPTCwKMhBKvvWqTvgfcSSnRzAWkLlQYSusayyZK4n9qcYkV5MFni1rbjx+Mr5aOEmb5u33amMKLwSTwIDAQABAoGBAIHAg6NJSiYC/NYpVzWfKlasuoNy78R5adXYSNZiCR5V5FNm5OzmODZgXUt6g0A7FomshIT/txQWoV7y5FmwPs8n13JY3Hdt4tJ6MHw2feLo710+OEp9VBQus3JsB2F8ONYrGvs00hPPL7h5av/rzTdE8F67YM1mSgeg7xEF6BghAkEA12OOqSzp2MLTNY7PqOaLDzy4aAMVNN3Ntv2jBN0jq7s1b5ilQ2PGkLwdtkicq/VZcRyUqVbZbMwz05II3nqx3wJBAMsVhRQ5sdFCRBzEbSAm2YEJaFh5u6QT3+zWHMFpPJRnaBAWz3RXKEnleJ+DS2Xz1Jm6ZrmLdZiwMx/8dK5rDZECQQC7GTdWi7ZC3dIcpwaKIGHRhZxmda8ZMkc9Wwwd8H7I8aFUZFPCu0xEc7SXoHHACit8zyfwBYpvMN8gPK3JnOkfAkEAsUSpk0wBMT38one7IZOHzCDgGkq4RbKrhdon45Pus0PIDDM9BrqFimtpbSN4DxhVfZK91DwtfAhhuAvv9cewYQJAPMhpAqv3PBGYmtRDUlWXJQv2JRJJkrvbbqgBed2OX5RRgj5V3SR6PBhLbcTZ+q+1tdPkMFzZo5U6MN5m/6oXvQ==", + "SOCIAL_AUTH_SAML_ORG_INFO": { + "en-US": {"name": "psa", "displayname": "PSA", "url": "https://github.com/omab/python-social-auth/"} + }, + "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": + {"givenName": "Tech Gal", "emailAddress": "technical@example.com"}, + "SOCIAL_AUTH_SAML_SUPPORT_CONTACT": + {"givenName": "Support Guy", "emailAddress": "support@example.com"}, + "SOCIAL_AUTH_SAML_ENABLED_IDPS": { + "testshib": { + "entity_id": "https://idp.testshib.org/idp/shibboleth", + "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", + "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYDVQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRlc3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7CyVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aTNPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWHgWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0GA1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ869nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBlbm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNoaWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRLI4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4/SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAjGeka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==" + }, + "other": { + "entity_id": "https://unused.saml.example.com", + "singleSignOnService": { + "url": "https://unused.saml.example.com/SAML2/Redirect/SSO", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } +} diff --git a/social/tests/backends/data/saml_response.txt b/social/tests/backends/data/saml_response.txt new file mode 100644 index 000000000..557bb59e8 --- /dev/null +++ b/social/tests/backends/data/saml_response.txt @@ -0,0 +1 @@ +http://myapp.com/?RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/social/tests/backends/test_saml.py b/social/tests/backends/test_saml.py new file mode 100644 index 000000000..abe256976 --- /dev/null +++ b/social/tests/backends/test_saml.py @@ -0,0 +1,104 @@ +import base64 +import datetime +from httpretty import HTTPretty +import json +from mock import patch +try: + from onelogin.saml2.utils import OneLogin_Saml2_Utils +except ImportError: + pass # Only available for python 2.7 at the moment, so don't worry if this fails +import os.path +import re +import requests +from social.p3 import urlparse +from social.utils import parse_qs, url_add_parameters +from social.tests.models import User +from social.tests.backends.base import BaseBackendTest +import sys +import unittest2 +try: + from urllib.parse import urlencode, urlparse, urlunparse, parse_qs +except ImportError: + from urllib import urlencode + from urlparse import urlparse, urlunparse, parse_qs + +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') + + +@unittest2.skipUnless( + sys.version_info[:2] == (2, 7), + "python-saml currently depends on 2.7; 3+ support coming soon") +@unittest2.skipIf('__pypy__' in sys.builtin_module_names, "dm.xmlsec not compatible with pypy") +class SAMLTest(BaseBackendTest): + backend_path = 'social.backends.saml.SAMLAuth' + expected_username = 'myself' + + def extra_settings(self): + with open(os.path.join(DATA_DIR, 'saml_config.json'), 'r') as config_file: + config_str = config_file.read() + return json.loads(config_str) + + def setUp(self): + """ Patch the time so that we can replay canned request/response pairs """ + super(SAMLTest, self).setUp() + + @staticmethod + def fixed_time(): + return OneLogin_Saml2_Utils.parse_SAML_to_time("2015-05-09T03:57:22Z") + now_patch = patch.object(OneLogin_Saml2_Utils, 'now', fixed_time) + now_patch.start() + self.addCleanup(now_patch.stop) + + def install_http_intercepts(self, start_url, return_url): + # When we request start_url (https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO...) + # we will eventually get a redirect back, with SAML assertion data in the query string. + # A pre-recorded correct response is kept in this .txt file: + with open(os.path.join(DATA_DIR, 'saml_response.txt'), 'r') as response_file: + response_url = response_file.read() + HTTPretty.register_uri(HTTPretty.GET, start_url, status=301, location=response_url) + HTTPretty.register_uri(HTTPretty.GET, return_url, status=200, body='foobar') + + def do_start(self): + # pretend we've started with a URL like /login/saml/?idp=testshib: + self.strategy.set_request_data({'idp': 'testshib'}, self.backend) + start_url = self.backend.start().url + # Modify the start URL to make the SAML request consistent from test to test: + start_url = self.modify_start_url(start_url) + # If the SAML Identity Provider recognizes the user, we will be redirected back to: + return_url = self.backend.redirect_uri + self.install_http_intercepts(start_url, return_url) + response = requests.get(start_url) + self.assertTrue(response.url.startswith(return_url)) + self.assertEqual(response.text, 'foobar') + query_values = dict((k, v[0]) for k, v in parse_qs(urlparse(response.url).query).items()) + self.assertNotIn(' ', query_values['SAMLResponse']) + self.strategy.set_request_data(query_values, self.backend) + return self.backend.complete() + + def test_metadata_generation(self): + """ Test that we can generate the metadata without error """ + xml, errors = self.backend.generate_metadata_xml() + self.assertEqual(len(errors), 0) + self.assertEqual(xml[0], '<') + + def test_login(self): + """ Test that we can authenticate with a SAML IdP (TestShib) """ + user = self.do_login() + + def modify_start_url(self, start_url): + """ + Given a SAML redirect URL, parse it and change the ID to + a consistent value, so the request is always identical. + """ + # Parse the SAML Request URL to get the XML being sent to TestShib + url_parts = urlparse(start_url) + query = dict((k, v[0]) for (k, v) in parse_qs(url_parts.query).iteritems()) + xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(query['SAMLRequest']) + # Modify the XML: + xml, changed = re.subn(r'ID="[^"]+"', 'ID="TEST_ID"', xml) + self.assertEqual(changed, 1) + # Update the URL to use the modified query string: + query['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) + url_parts = list(url_parts) + url_parts[4] = urlencode(query) + return urlunparse(url_parts) diff --git a/social/tests/models.py b/social/tests/models.py index 7dae52f75..80bf6871e 100644 --- a/social/tests/models.py +++ b/social/tests/models.py @@ -117,7 +117,7 @@ def get_social_auth(cls, provider, uid): @classmethod def get_social_auth_for_user(cls, user, provider=None, id=None): - return user.social + return [usa for usa in user.social if provider in (None, usa.provider) and id in (None, usa.id)] @classmethod def create_social_auth(cls, user, uid, provider): diff --git a/social/tests/strategy.py b/social/tests/strategy.py index 9ccd7d04f..d88685f2d 100644 --- a/social/tests/strategy.py +++ b/social/tests/strategy.py @@ -50,6 +50,26 @@ def request_host(self): """Return current host value""" return TEST_HOST + def request_is_secure(self): + """ Is the request using HTTPS? """ + return False + + def request_path(self): + """ path of the current request """ + return '' + + def request_port(self): + """ Port in use for this request """ + return 80 + + def request_get(self): + """ Request GET data """ + return self._request_data.copy() + + def request_post(self): + """ Request POST data """ + return self._request_data.copy() + def session_get(self, name, default=None): """Return session value for given key""" return self._session.get(name, default) From 17def186d4bb7165f9c37037936997ef39ae2f29 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Mon, 11 May 2015 20:38:55 -0700 Subject: [PATCH 586/890] Add python-saml requirement (temporary commit) --- setup.py | 2 +- social/tests/requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8860b0a0b..a706a77cd 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ def get_packages(): requirements = f.readlines() with open(tests_requirements_file, 'r') as f: - tests_requirements = f.readlines() + tests_requirements = [line for line in f.readlines() if '@' not in line] setup( name='python-social-auth', diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index 33cef2575..12a8d0dad 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -6,3 +6,4 @@ rednose>=0.4.1 requests>=1.1.0 PyJWT>=1.0.0,<2.0.0 unittest2==0.5.1 +git+https://github.com/open-craft/python-saml.git@9602b8133056d8c3caa7c3038761147df3d4b257#egg=python-saml From 04f5ff30222970e9ccf618ec480eec00448a2fcb Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Sat, 16 May 2015 14:47:23 -0400 Subject: [PATCH 587/890] Add a DigitalOcean backend. --- README.rst | 2 ++ docs/backends/digitalocean.rst | 24 +++++++++++++ docs/backends/index.rst | 1 + social/backends/digitalocean.py | 41 ++++++++++++++++++++++ social/tests/backends/test_digitalocean.py | 34 ++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 docs/backends/digitalocean.rst create mode 100644 social/backends/digitalocean.py create mode 100644 social/tests/backends/test_digitalocean.py diff --git a/README.rst b/README.rst index 73f6cf715..ef2e99254 100644 --- a/README.rst +++ b/README.rst @@ -63,6 +63,7 @@ or current ones extended): * Clef_ OAuth2 * Coursera_ OAuth2 * Dailymotion_ OAuth2 + * DigitalOcean_ OAuth2 https://developers.digitalocean.com/documentation/oauth/ * Disqus_ OAuth2 * Douban_ OAuth1 and OAuth2 * Dropbox_ OAuth1 and OAuth2 @@ -240,6 +241,7 @@ check `django-social-auth LICENSE`_ for details: .. _Clef: https://getclef.com/ .. _Coursera: https://www.coursera.org/ .. _Dailymotion: https://dailymotion.com +.. _DigitalOcean: https://www.digitalocean.com/ .. _Disqus: https://disqus.com .. _Douban: http://www.douban.com .. _Dropbox: https://dropbox.com diff --git a/docs/backends/digitalocean.rst b/docs/backends/digitalocean.rst new file mode 100644 index 000000000..f50f41324 --- /dev/null +++ b/docs/backends/digitalocean.rst @@ -0,0 +1,24 @@ +DigitalOcean +============ + +DigitalOcean uses OAuth2 for its auth process. See the full `DigitalOcean +developer's documentation`_ for more information. + +- Register a new application in the `Apps & API page`_ in the DigitalOcean + control panel, setting the callback URL to ``http://example.com/complete/digitalocean/`` + replacing ``example.com`` with your domain. + +- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings:: + + SOCIAL_AUTH_DIGITALOCEAN_KEY = '' + SOCIAL_AUTH_DIGITALOCEAN_SECRET = '' + +- By default, only ``read`` permissions are granted. In order to create, + destroy, and take other actions on the user's resources, you must request + ``read write`` permissions like so:: + + SOCIAL_AUTH_DIGITALOCEAN_AUTH_EXTRA_ARGUMENTS = {'scope': 'read write'} + + +.. _DigitalOcean developer's documentation: https://developers.digitalocean.com/documentation/ +.. _Apps & API page: https://cloud.digitalocean.com/settings/applications \ No newline at end of file diff --git a/docs/backends/index.rst b/docs/backends/index.rst index d42e414ef..0de856497 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -60,6 +60,7 @@ Social backends coinbase coursera dailymotion + digitalocean disqus docker douban diff --git a/social/backends/digitalocean.py b/social/backends/digitalocean.py new file mode 100644 index 000000000..780e4be6e --- /dev/null +++ b/social/backends/digitalocean.py @@ -0,0 +1,41 @@ +from social.backends.oauth import BaseOAuth2 + + +class DigitalOceanOAuth(BaseOAuth2): + """ + DigitalOcean OAuth authentication backend. + + Docs: https://developers.digitalocean.com/documentation/oauth/ + """ + name = 'digitalocean' + AUTHORIZATION_URL = 'https://cloud.digitalocean.com/v1/oauth/authorize' + ACCESS_TOKEN_URL = 'https://cloud.digitalocean.com/v1/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ' ' + EXTRA_DATA = [ + ('expires_in', 'expires_in') + ] + + def get_user_id(self, details, response): + """Return user unique id provided by service""" + return response['account'].get('uuid') + + def get_user_details(self, response): + """Return user details from DigitalOcean account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') or '') + + return {'username': response['account'].get('email'), + 'email': response['account'].get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, token, *args, **kwargs): + """Loads user data from service""" + url = 'https://api.digitalocean.com/v2/account' + auth_header = {"Authorization": "Bearer %s" % token} + try: + return self.get_json(url, headers=auth_header) + except ValueError: + return None diff --git a/social/tests/backends/test_digitalocean.py b/social/tests/backends/test_digitalocean.py new file mode 100644 index 000000000..5eeedb310 --- /dev/null +++ b/social/tests/backends/test_digitalocean.py @@ -0,0 +1,34 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class DigitalOceanOAuthTest(OAuth2Test): + backend_path = 'social.backends.digitalocean.DigitalOceanOAuth' + user_data_url = 'https://api.digitalocean.com/v2/account' + expected_username = 'sammy@digitalocean.com' + access_token_body = json.dumps({ + 'access_token': '547cac21118ae7', + 'token_type': 'bearer', + 'expires_in': 2592000, + 'refresh_token': '00a3aae641658d', + 'scope': 'read write', + 'info': { + 'name': 'Sammy Shark', + 'email': 'sammy@digitalocean.com' + } + }) + user_data_body = json.dumps({ + "account": { + 'droplet_limit': 25, + 'email': 'sammy@digitalocean.com', + 'uuid': 'b6fr89dbf6d9156cace5f3c78dc9851d957381ef', + 'email_verified': True + } + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From d5a62c84ea0878e3fc309f8a6e999f5992a2b815 Mon Sep 17 00:00:00 2001 From: duoduo369 Date: Sun, 17 May 2015 15:40:01 +0800 Subject: [PATCH 588/890] add weixin backends Weixin is Tencent's icq, in China there are million's people use it. --- social/backends/weixin.py | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 social/backends/weixin.py diff --git a/social/backends/weixin.py b/social/backends/weixin.py new file mode 100644 index 000000000..5170cafd2 --- /dev/null +++ b/social/backends/weixin.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# author:duoduo3369@gmail.com https://github.com/duoduo369 +""" +Weixin OAuth2 backend +""" +from requests import HTTPError + +from social.backends.oauth import BaseOAuth2 +from social.exceptions import AuthCanceled, AuthUnknownError + + +class WeixinOAuth2(BaseOAuth2): + """Weixin OAuth authentication backend""" + name = 'weixin' + ID_KEY = 'openid' + AUTHORIZATION_URL = 'https://open.weixin.qq.com/connect/qrconnect' + ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('nickname', 'username'), + ('headimgurl', 'profile_image_url'), + ] + + def get_user_details(self, response): + """Return user details from Weixin. API URL is: + https://api.weixin.qq.com/sns/userinfo + """ + if self.setting('DOMAIN_AS_USERNAME'): + username = response.get('domain', '') + else: + username = response.get('nickname', '') + profile_image_url = response.get('headimgurl', '') + return {'username': username, 'profile_image_url': profile_image_url} + + def user_data(self, access_token, *args, **kwargs): + data = self.get_json('https://api.weixin.qq.com/sns/userinfo', + params={'access_token': access_token, + 'openid': kwargs['response']['openid']}) + nickname = data.get('nickname') + if nickname: + # weixin api has some encode bug, here need handle + data['nickname'] = nickname.encode('raw_unicode_escape').decode('utf-8') + return data + + + def auth_params(self, state=None): + appid, secret = self.get_key_and_secret() + params = { + 'appid': appid, + 'redirect_uri': self.get_redirect_uri(state) + } + if self.STATE_PARAMETER and state: + params['state'] = state + if self.RESPONSE_TYPE: + params['response_type'] = self.RESPONSE_TYPE + return params + + def auth_complete_params(self, state=None): + appid, secret = self.get_key_and_secret() + return { + 'grant_type': 'authorization_code', # request auth code + 'code': self.data.get('code', ''), # server response code + 'appid': appid, + 'secret': secret, + 'redirect_uri': self.get_redirect_uri(state) + } + + def refresh_token_params(self, token, *args, **kwargs): + appid, secret = self.get_key_and_secret() + return { + 'refresh_token': token, + 'grant_type': 'refresh_token', + 'appid': appid, + 'secret': secret + } + + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + self.process_error(self.data) + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + raise + except KeyError: + raise AuthUnknownError(self) + if 'errcode' in response: + raise AuthCanceled(self) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) From cb82dfb7ef1c0273acc4aabb32c34317dcd8de4a Mon Sep 17 00:00:00 2001 From: blurrcat Date: Wed, 20 May 2015 09:35:36 +0800 Subject: [PATCH 589/890] fix Fitbit OAuth 1 authorization URL According to https://wiki.fitbit.com/display/API/OAuth+1.0a+Authentication#OAuth1.0aAuthentication-Notes, the base url for the oauth authroization page is https://www.fitbit.com/, not https://api.fitbit.com. Soon they will redirect authorize requests to api.fitbit.com to www.fitbit.com, which will increase page load time for end users --- social/backends/fitbit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/fitbit.py b/social/backends/fitbit.py index 1e808988b..655a711e5 100644 --- a/social/backends/fitbit.py +++ b/social/backends/fitbit.py @@ -8,7 +8,7 @@ class FitbitOAuth(BaseOAuth1): """Fitbit OAuth authentication backend""" name = 'fitbit' - AUTHORIZATION_URL = 'https://api.fitbit.com/oauth/authorize' + AUTHORIZATION_URL = 'https://www.fitbit.com/oauth/authorize' REQUEST_TOKEN_URL = 'https://api.fitbit.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.fitbit.com/oauth/access_token' ID_KEY = 'encodedId' From 80ceb11e761c8a154b00e488e26412cacac1d1f9 Mon Sep 17 00:00:00 2001 From: Marek Jalovec Date: Wed, 20 May 2015 10:51:41 +0200 Subject: [PATCH 590/890] Fixes "ImportError: No module named packages.urllib3.poolmanager" error (fixes #617) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f525ea7b..1d05eedeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ python-openid>=2.2 -requests>=1.1.0 +requests>=2.5.1 oauthlib>=0.3.8 requests-oauthlib>=0.3.1 six>=1.2.0 From b6b557eed3ed7cfd630d329c165ae7d3fe1355f1 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 20 May 2015 10:41:07 -0700 Subject: [PATCH 591/890] Make IdP name format slightly more flexible --- social/backends/saml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index 135cf4086..0a50c96ed 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -28,7 +28,8 @@ class SAMLIdentityProvider(object): def __init__(self, name, **kwargs): """ Load and parse configuration """ self.name = name - assert self.name.isalnum() # If 'name' contained a colon, it would affect our UID mangling + # name should be a slug and must not contain a colon, which could conflict with uid prefixing: + assert ':' not in self.name and ' ' not in self.name, "IdP 'name' should be a slug (short, no spaces)" self.conf = kwargs def get_user_permanent_id(self, attributes): From de8152360b9f1c644fa3192bd601cf9452cd6a22 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 20 May 2015 23:47:46 -0700 Subject: [PATCH 592/890] Add an integration point for extra security layers like eduPersonEntitlement --- social/backends/saml.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index 0a50c96ed..703a886e8 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -14,6 +14,7 @@ # Helpful constants: OID_COMMON_NAME = "urn:oid:2.5.4.3" OID_EDU_PERSON_PRINCIPAL_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" +OID_EDU_PERSON_ENTITLEMENT = "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" OID_GIVEN_NAME = "urn:oid:2.5.4.42" OID_MAIL = "urn:oid:0.9.2342.19200300.100.1.3" OID_SURNAME = "urn:oid:2.5.4.4" @@ -210,11 +211,11 @@ def saml_metadata_view(request): errors = saml_settings.validate_metadata(metadata) return metadata, errors - def _create_saml_auth(self, idp_name): + def _create_saml_auth(self, idp): """ Get an instance of OneLogin_Saml2_Auth """ - config = self.generate_saml_config(idp=self.get_idp(idp_name)) + config = self.generate_saml_config(idp) request_info = { 'https': 'on' if self.strategy.request_is_secure() else 'off', 'http_host': self.strategy.request_host(), @@ -228,7 +229,7 @@ def _create_saml_auth(self, idp_name): def auth_url(self): """ Get the URL to which we must redirect in order to authenticate the user """ idp_name = self.strategy.request_data()['idp'] - auth = self._create_saml_auth(idp_name) + auth = self._create_saml_auth(idp=self.get_idp(idp_name)) # Below, return_to sets the RelayState, which can contain arbitrary data. # We use it to store the specific SAML IdP backend name, since we combine # many backends to a single URL. @@ -257,7 +258,8 @@ def auth_complete(self, *args, **kwargs): everything checks out. """ idp_name = self.strategy.request_data()['RelayState'] - auth = self._create_saml_auth(idp_name) + idp = self.get_idp(idp_name) + auth = self._create_saml_auth(idp) auth.process_response() errors = auth.get_errors() if errors or not auth.is_authenticated(): @@ -267,6 +269,8 @@ def auth_complete(self, *args, **kwargs): attributes = auth.get_attributes() attributes['name_id'] = auth.get_nameid() + self._check_entitlements(idp, attributes) + response = { 'idp_name': idp_name, 'attributes': attributes, @@ -276,3 +280,15 @@ def auth_complete(self, *args, **kwargs): kwargs.update({'response': response, 'backend': self}) return self.strategy.authenticate(*args, **kwargs) + + def _check_entitlements(self, idp, attributes): + """ + Additional verification of a SAML response before authenticating the user. + + Subclasses can override this method if they need custom validation code, + such as requiring the presence of an eduPersonEntitlement. + + raise social.exceptions.AuthForbidden if the user should not be authenticated, + or do nothing to allow the login pipeline to continue. + """ + pass From 6dd502464b1e7e77fd33ca3f4bf8990f0271fdd9 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 20 May 2015 23:48:07 -0700 Subject: [PATCH 593/890] Minor consistency fix --- social/backends/saml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index 703a886e8..c6ea5c95c 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -40,7 +40,7 @@ def get_user_permanent_id(self, attributes): If you want to use the NameID, it's available via attributes['name_id'] """ - return attributes[self.conf.get('user_permanent_id', OID_USERID)][0] + return attributes[self.conf.get('attr_user_permanent_id', OID_USERID)][0] # Attributes processing: def get_user_details(self, attributes): From 02ab628b8961b969021de87aeb23551da4e751b7 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Thu, 21 May 2015 12:08:10 -0700 Subject: [PATCH 594/890] Minor cleanups --- social/backends/saml.py | 13 ++++--------- social/tests/backends/data/saml_config.json | 5 +---- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index c6ea5c95c..0ea11c07a 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -74,11 +74,6 @@ def sso_url(self): """ Get the SSO URL for this IdP """ return self.conf['url'] # Required. e.g. "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" - @property - def sso_binding(self): - """ Get the method used to submit our request to the SSO URL """ - return self.conf.get('binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect') - @property def x509cert(self): """ X.509 Public Key Certificate for this IdP """ @@ -91,7 +86,7 @@ def saml_config_dict(self): "entityId": self.entity_id, "singleSignOnService": { "url": self.sso_url, - "binding": self.sso_binding, + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", # python-saml only supports Redirect }, "x509cert": self.x509cert, } @@ -174,7 +169,7 @@ def generate_saml_config(self, idp): "sp": { "assertionConsumerService": { "url": abs_completion_url, - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", # python-saml only supports HTTP-POST }, "entityId": self.setting("SP_ENTITY_ID"), "NameIDFormats": self.setting("SP_NAMEID_FORMATS", []), @@ -231,8 +226,8 @@ def auth_url(self): idp_name = self.strategy.request_data()['idp'] auth = self._create_saml_auth(idp=self.get_idp(idp_name)) # Below, return_to sets the RelayState, which can contain arbitrary data. - # We use it to store the specific SAML IdP backend name, since we combine - # many backends to a single URL. + # We use it to store the specific SAML IdP name, since we multiple IdPs + # share the same auth_complete URL. return auth.login(return_to=idp_name) def get_user_details(self, response): diff --git a/social/tests/backends/data/saml_config.json b/social/tests/backends/data/saml_config.json index 5c119e6a7..3f610107c 100644 --- a/social/tests/backends/data/saml_config.json +++ b/social/tests/backends/data/saml_config.json @@ -17,10 +17,7 @@ }, "other": { "entity_id": "https://unused.saml.example.com", - "singleSignOnService": { - "url": "https://unused.saml.example.com/SAML2/Redirect/SSO", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - } + "url": "https://unused.saml.example.com/SAML2/Redirect/SSO" } } } From 92f259b4eec126a418570ef10a2ea3d653868450 Mon Sep 17 00:00:00 2001 From: vinhub Date: Mon, 11 May 2015 12:04:28 -0700 Subject: [PATCH 595/890] Added provider for Microsoft Azure Active Directory OAuth2 --- social/backends/azuread.py | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 social/backends/azuread.py diff --git a/social/backends/azuread.py b/social/backends/azuread.py new file mode 100644 index 000000000..c0e6bed02 --- /dev/null +++ b/social/backends/azuread.py @@ -0,0 +1,79 @@ +""" +Azure AD OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/azuread.html +""" +import datetime +from calendar import timegm +from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ + AuthUnknownError, AuthMissingParameter, \ + AuthTokenError +from jwt import DecodeError, ExpiredSignature, decode as jwt_decode +from social.backends.oauth import BaseOAuth2 +import urllib + +class AzureADOAuth2(BaseOAuth2): + name = 'azuread-oauth2' + SCOPE_SEPARATOR = ' ' + AUTHORIZATION_URL = 'https://login.windows.net/common/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://login.windows.net/common/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + DEFAULT_SCOPE = ['openid', 'profile', 'user_impersonation'] + EXTRA_DATA = [ + ('access_token', 'access_token'), + ('id_token', 'id_token'), + ('refresh_token', 'refresh_token'), + ('expires_in', 'expires'), + ('given_name', 'first_name'), + ('family_name', 'last_name'), + ('token_type', 'token_type') + ] + + def auth_extra_arguments(self): + """Return extra arguments needed on auth process. The defaults can be + overriden by GET parameters.""" + extra_arguments = {} + resource = self.setting('SHAREPOINT_SITE') + + if resource: + extra_arguments = { + 'resource': resource + } + + return extra_arguments + + def get_user_id(self, details, response): + """Use upn as unique id""" + return response.get('upn') + + def get_user_details(self, response): + """Return user details from Azure AD account""" + fullname, first_name, last_name = ( + response.get('name', ''), + response.get('given_name', ''), + response.get('family_name', '') + ) + return {'username': fullname, + 'email': response.get('upn'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + response = kwargs.get('response') + id_token = response.get('id_token') + + try: + decoded_id_token = jwt_decode(id_token, verify=False) + except (DecodeError, ExpiredSignature) as de: + raise AuthTokenError(self, de) + + return decoded_id_token + + def extra_data(self, user, uid, response, details=None): + """Return access_token and extra defined names to store in + extra_data field""" + data = super(BaseOAuth2, self).extra_data(user, uid, response, details) + data['sharepoint_site'] = self.setting('SHAREPOINT_SITE') + return data + From c748cbffcf46aa245a73bc610438b6bcc0e3cfa4 Mon Sep 17 00:00:00 2001 From: vinhub Date: Mon, 11 May 2015 12:31:05 -0700 Subject: [PATCH 596/890] cleaned up unneeded Sharepoint related code --- social/backends/azuread.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/social/backends/azuread.py b/social/backends/azuread.py index c0e6bed02..b716ae30f 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -29,19 +29,6 @@ class AzureADOAuth2(BaseOAuth2): ('token_type', 'token_type') ] - def auth_extra_arguments(self): - """Return extra arguments needed on auth process. The defaults can be - overriden by GET parameters.""" - extra_arguments = {} - resource = self.setting('SHAREPOINT_SITE') - - if resource: - extra_arguments = { - 'resource': resource - } - - return extra_arguments - def get_user_id(self, details, response): """Use upn as unique id""" return response.get('upn') @@ -74,6 +61,5 @@ def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in extra_data field""" data = super(BaseOAuth2, self).extra_data(user, uid, response, details) - data['sharepoint_site'] = self.setting('SHAREPOINT_SITE') return data From 0a1c8a7ea5da636f5619962e73bd9b9f433f54de Mon Sep 17 00:00:00 2001 From: vinhub Date: Mon, 11 May 2015 14:52:58 -0700 Subject: [PATCH 597/890] added generic resource setting added license text added documentation file --- docs/backends/azuread.rst | 19 +++++++++++++++++++ license.txt | 23 +++++++++++++++++++++++ social/backends/azuread.py | 15 ++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 docs/backends/azuread.rst create mode 100644 license.txt diff --git a/docs/backends/azuread.rst b/docs/backends/azuread.rst new file mode 100644 index 000000000..8ec245777 --- /dev/null +++ b/docs/backends/azuread.rst @@ -0,0 +1,19 @@ +Microsoft Azure Active Directory +====== + +To enable OAuth2 support: + +- Fill in ``Client ID`` and ``Client Secret`` settings. These values can be obtained + easily as described in `Azure AD Application Registration`_ doc:: + + SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '' + SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_AZUREAD_OAUTH2_RESOURCE = '' + + This is the resource you would like to access after authentication succeeds. + Some of the possible values are: ``https://graph.windows.net`` or ``https://-my.sharepoint.com``. + +.. _Azure AD Application Registration: https://msdn.microsoft.com/en-us/library/azure/dn132599.aspx diff --git a/license.txt b/license.txt new file mode 100644 index 000000000..d92182403 --- /dev/null +++ b/license.txt @@ -0,0 +1,23 @@ +Copyright (c) 2015 Microsoft Open Technologies, Inc. + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/social/backends/azuread.py b/social/backends/azuread.py index b716ae30f..769a0f633 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -57,9 +57,22 @@ def user_data(self, access_token, *args, **kwargs): return decoded_id_token + def auth_extra_arguments(self): + """Return extra arguments needed on auth process. The defaults can be + overriden by GET parameters.""" + extra_arguments = {} + resource = self.setting('RESOURCE') + + if resource: + extra_arguments = { + 'resource': resource + } + + return extra_arguments + def extra_data(self, user, uid, response, details=None): """Return access_token and extra defined names to store in extra_data field""" data = super(BaseOAuth2, self).extra_data(user, uid, response, details) + data['resource'] = self.setting('RESOURCE') return data - From b149d29f299f52e9804c3d132f50e8d69555c815 Mon Sep 17 00:00:00 2001 From: vinhub Date: Tue, 12 May 2015 11:34:33 -0700 Subject: [PATCH 598/890] added test cases for AzureADOAuth2 refresh token method added --- social/backends/azuread.py | 17 ++++++++++++ social/tests/backends/test_azuread.py | 40 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 social/tests/backends/test_azuread.py diff --git a/social/backends/azuread.py b/social/backends/azuread.py index 769a0f633..1311139fc 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -9,6 +9,7 @@ AuthTokenError from jwt import DecodeError, ExpiredSignature, decode as jwt_decode from social.backends.oauth import BaseOAuth2 +import requests import urllib class AzureADOAuth2(BaseOAuth2): @@ -76,3 +77,19 @@ def extra_data(self, user, uid, response, details=None): data = super(BaseOAuth2, self).extra_data(user, uid, response, details) data['resource'] = self.setting('RESOURCE') return data + + def refresh_token_params(self, token, *args, **kwargs): + return { + 'refresh_token': token, + 'grant_type': 'refresh_token', + 'resource': self.setting('RESOURCE') + } + + def get_auth_token(self, token): + response = requests.get('https://graph.windows.net/me', headers={'Authorization': 'Bearer ' + token}) + + if response.status_code == 401: + new_token_response = self.refresh_token(token) + token = new_token_response['access_token'] + + return token diff --git a/social/tests/backends/test_azuread.py b/social/tests/backends/test_azuread.py new file mode 100644 index 000000000..57df71aad --- /dev/null +++ b/social/tests/backends/test_azuread.py @@ -0,0 +1,40 @@ +import json +from social.p3 import urlencode +from social.tests.backends.oauth import OAuth2Test + +class AzureADOAuth2Test(OAuth2Test): + + backend_path = 'social.backends.azuread.AzureADOAuth2' + user_data_url = 'https://graph.windows.net/me' + expected_username = 'foobar' + + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer', + 'id_token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83Mjc0MDZhYy03MDY4' + 'LTQ4ZmEtOTJiOS1jMmQ2NzIxMWJjNTAvIiwiaWF0IjpudWxsLCJleHAiOm51bGwsImF1ZCI6IjAyOWNjMDEwLWJiNzQtNGQyY' + 'i1hMDQwLWY5Y2VkM2ZkMmM3NiIsInN1YiI6InFVOHhrczltSHFuVjZRMzR6aDdTQVpvY2loOUV6cnJJOW1wVlhPSWJWQTgiLC' + 'J2ZXIiOiIxLjAiLCJ0aWQiOiI3Mjc0MDZhYy03MDY4LTQ4ZmEtOTJiOS1jMmQ2NzIxMWJjNTAiLCJvaWQiOiI3ZjhlMTk2OS0' + '4YjgxLTQzOGMtOGQ0ZS1hZDZmNTYyYjI4YmIiLCJ1cG4iOiJmb29iYXJAdGVzdC5vbm1pY3Jvc29mdC5jb20iLCJnaXZlbl9u' + 'YW1lIjoiZm9vIiwiZmFtaWx5X25hbWUiOiJiYXIiLCJuYW1lIjoiZm9vIGJhciIsInVuaXF1ZV9uYW1lIjoiZm9vYmFyQHRlc' + '3Qub25taWNyb3NvZnQuY29tIiwicHdkX2V4cCI6IjQ3MzMwOTY4IiwicHdkX3VybCI6Imh0dHBzOi8vcG9ydGFsLm1pY3Jvc2' + '9mdG9ubGluZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn2g7BjX5JxNe8skYlK4PdhiLz4' + }) + + refresh_token_body = json.dumps({ + 'access_token': 'foobar-new-token', + 'token_type': 'bearer', + 'expires_in': 3600.0, + 'refresh_token': 'foobar-new-refresh-token', + 'scope': 'identity' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() + + def test_refresh_token(self): + user, social = self.do_refresh_token() + self.assertEqual(social.extra_data['access_token'], 'foobar-new-token') \ No newline at end of file From 36051d6df1a6ba6e80b4623540cec3c9d30f0371 Mon Sep 17 00:00:00 2001 From: vinhub Date: Wed, 13 May 2015 10:05:06 -0700 Subject: [PATCH 599/890] added get_auth_token method for ease of use --- social/backends/azuread.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/social/backends/azuread.py b/social/backends/azuread.py index 1311139fc..42ead2d26 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -10,6 +10,7 @@ from jwt import DecodeError, ExpiredSignature, decode as jwt_decode from social.backends.oauth import BaseOAuth2 import requests +import time import urllib class AzureADOAuth2(BaseOAuth2): @@ -25,6 +26,8 @@ class AzureADOAuth2(BaseOAuth2): ('id_token', 'id_token'), ('refresh_token', 'refresh_token'), ('expires_in', 'expires'), + ('expires_on', 'expires_on'), + ('not_before', 'not_before'), ('given_name', 'first_name'), ('family_name', 'last_name'), ('token_type', 'token_type') @@ -85,11 +88,17 @@ def refresh_token_params(self, token, *args, **kwargs): 'resource': self.setting('RESOURCE') } - def get_auth_token(self, token): - response = requests.get('https://graph.windows.net/me', headers={'Authorization': 'Bearer ' + token}) + def get_auth_token(self, user_id): + """Return the access token for the given user, after ensuring that it has not expired, + or refreshing it if so.""" + user = self.get_user(user_id=user_id) - if response.status_code == 401: - new_token_response = self.refresh_token(token) - token = new_token_response['access_token'] + access_token = user.social_user.access_token + expires_on = user.social_user.extra_data['expires_on'] + + if expires_on <= int(time.time()): + new_token_response = self.refresh_token(token=access_token) + access_token = new_token_response['access_token'] + + return access_token - return token From 45b654b6c49203fd96410200dc67d4caed303ba8 Mon Sep 17 00:00:00 2001 From: vinhub Date: Fri, 15 May 2015 10:47:26 -0700 Subject: [PATCH 600/890] updated test cases --- social/tests/backends/test_azuread.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/social/tests/backends/test_azuread.py b/social/tests/backends/test_azuread.py index 57df71aad..fd7561af6 100644 --- a/social/tests/backends/test_azuread.py +++ b/social/tests/backends/test_azuread.py @@ -1,6 +1,8 @@ import json from social.p3 import urlencode from social.tests.backends.oauth import OAuth2Test +from httpretty import HTTPretty +from social.tests.models import User class AzureADOAuth2Test(OAuth2Test): @@ -18,13 +20,16 @@ class AzureADOAuth2Test(OAuth2Test): '4YjgxLTQzOGMtOGQ0ZS1hZDZmNTYyYjI4YmIiLCJ1cG4iOiJmb29iYXJAdGVzdC5vbm1pY3Jvc29mdC5jb20iLCJnaXZlbl9u' 'YW1lIjoiZm9vIiwiZmFtaWx5X25hbWUiOiJiYXIiLCJuYW1lIjoiZm9vIGJhciIsInVuaXF1ZV9uYW1lIjoiZm9vYmFyQHRlc' '3Qub25taWNyb3NvZnQuY29tIiwicHdkX2V4cCI6IjQ3MzMwOTY4IiwicHdkX3VybCI6Imh0dHBzOi8vcG9ydGFsLm1pY3Jvc2' - '9mdG9ubGluZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn2g7BjX5JxNe8skYlK4PdhiLz4' + '9mdG9ubGluZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn2g7BjX5JxNe8skYlK4PdhiLz4', + 'expires_in': 3600, + 'expires_on': 1423650396, + 'not_before': 1423646496 }) refresh_token_body = json.dumps({ 'access_token': 'foobar-new-token', 'token_type': 'bearer', - 'expires_in': 3600.0, + 'expires_in': 3600, 'refresh_token': 'foobar-new-refresh-token', 'scope': 'identity' }) @@ -37,4 +42,17 @@ def test_partial_pipeline(self): def test_refresh_token(self): user, social = self.do_refresh_token() - self.assertEqual(social.extra_data['access_token'], 'foobar-new-token') \ No newline at end of file + self.assertEqual(social.extra_data['access_token'], 'foobar-new-token') + + # TODO: + # def test_get_access_token(self): + # self.do_login() + # HTTPretty.register_uri(self._method(self.backend.REFRESH_TOKEN_METHOD), + # self.backend.REFRESH_TOKEN_URL or + # self.backend.ACCESS_TOKEN_URL, + # status=200, + # body=self.refresh_token_body) + # user = list(User.cache.values())[0] + # token = self.backend.get_auth_token(user_id=user.id) + # self.assertEqual(token, 'foobar-new-token') + \ No newline at end of file From af7574f480e79cf96284fb8704f1e20111efec1d Mon Sep 17 00:00:00 2001 From: vinhub Date: Mon, 18 May 2015 17:50:20 -0700 Subject: [PATCH 601/890] added copyright / license notices --- social/backends/azuread.py | 26 ++++++++++++++++++++++++++ social/tests/backends/test_azuread.py | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/social/backends/azuread.py b/social/backends/azuread.py index 42ead2d26..b34b3eb07 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -1,3 +1,29 @@ +""" +Copyright (c) 2015 Microsoft Open Technologies, Inc. + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + """ Azure AD OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/azuread.html diff --git a/social/tests/backends/test_azuread.py b/social/tests/backends/test_azuread.py index fd7561af6..e2e36a4c5 100644 --- a/social/tests/backends/test_azuread.py +++ b/social/tests/backends/test_azuread.py @@ -1,3 +1,29 @@ +""" +Copyright (c) 2015 Microsoft Open Technologies, Inc. + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + import json from social.p3 import urlencode from social.tests.backends.oauth import OAuth2Test From 5f1cefdce73c688ab1a395a3f09b41631e78fa67 Mon Sep 17 00:00:00 2001 From: sushantgawali Date: Mon, 25 May 2015 22:22:52 +0530 Subject: [PATCH 602/890] changes in extra_data --- social/backends/azuread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/azuread.py b/social/backends/azuread.py index b34b3eb07..33e820dcc 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -100,10 +100,10 @@ def auth_extra_arguments(self): return extra_arguments - def extra_data(self, user, uid, response, details=None): + def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" - data = super(BaseOAuth2, self).extra_data(user, uid, response, details) + data = super(AzureADOAuth2, self).extra_data(user, uid, response,details, *args, **kwargs) data['resource'] = self.setting('RESOURCE') return data From 7ddb1102fc7f0944165c7a71402bc07681292074 Mon Sep 17 00:00:00 2001 From: "Vinayak (Vin) Bhalerao" Date: Tue, 26 May 2015 18:57:14 -0700 Subject: [PATCH 603/890] Removed --- license.txt | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 license.txt diff --git a/license.txt b/license.txt deleted file mode 100644 index d92182403..000000000 --- a/license.txt +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2015 Microsoft Open Technologies, Inc. - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. From 1e5e13b7890719d230dbff20b765e04f88258e0e Mon Sep 17 00:00:00 2001 From: vinhub Date: Tue, 26 May 2015 19:08:21 -0700 Subject: [PATCH 604/890] Added Azure AD docs to index.rst --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index d42e414ef..81d031101 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -50,6 +50,7 @@ Social backends angel aol appsfuel + azuread battlenet beats behance From b53e4dd48116e255f0a1e145f4e9d2bd21cd3722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 13:11:04 -0300 Subject: [PATCH 605/890] Avoid storing empty values from user details --- social/pipeline/user.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index f4fcd643e..215791719 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -83,12 +83,11 @@ def user_details(strategy, details, user=None, *args, **kwargs): # example username and id fields. It's also possible to disable update # on fields defined in SOCIAL_AUTH_PROTECTED_FIELDS. for name, value in details.items(): - if not hasattr(user, name): - continue - current_value = getattr(user, name, None) - if not current_value or name not in protected: - changed |= current_value != value - setattr(user, name, value) + if value and hasattr(user, name): + current_value = getattr(user, name, None) + if current_value is None or name not in protected: + changed |= current_value != value + setattr(user, name, value) if changed: strategy.storage.user.changed(user) From 0c0eb172b240774d254436ccfdd6e5c39da151df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 13:28:14 -0300 Subject: [PATCH 606/890] PEP8 --- docs/backends/azuread.rst | 11 ++++++----- social/backends/azuread.py | 34 +++++++++++----------------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/docs/backends/azuread.rst b/docs/backends/azuread.rst index 8ec245777..6c84c5cc3 100644 --- a/docs/backends/azuread.rst +++ b/docs/backends/azuread.rst @@ -1,10 +1,10 @@ Microsoft Azure Active Directory -====== +================================ To enable OAuth2 support: -- Fill in ``Client ID`` and ``Client Secret`` settings. These values can be obtained - easily as described in `Azure AD Application Registration`_ doc:: +- Fill in ``Client ID`` and ``Client Secret`` settings. These values can be + obtained easily as described in `Azure AD Application Registration`_ doc:: SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '' SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '' @@ -13,7 +13,8 @@ To enable OAuth2 support: SOCIAL_AUTH_AZUREAD_OAUTH2_RESOURCE = '' - This is the resource you would like to access after authentication succeeds. - Some of the possible values are: ``https://graph.windows.net`` or ``https://-my.sharepoint.com``. + This is the resource you would like to access after authentication succeeds. + Some of the possible values are: ``https://graph.windows.net`` or + ``https://-my.sharepoint.com``. .. _Azure AD Application Registration: https://msdn.microsoft.com/en-us/library/azure/dn132599.aspx diff --git a/social/backends/azuread.py b/social/backends/azuread.py index 33e820dcc..a015a0fc9 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -1,7 +1,7 @@ """ Copyright (c) 2015 Microsoft Open Technologies, Inc. -All rights reserved. +All rights reserved. MIT License @@ -28,16 +28,13 @@ Azure AD OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/azuread.html """ -import datetime -from calendar import timegm -from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthMissingParameter, \ - AuthTokenError +import time + from jwt import DecodeError, ExpiredSignature, decode as jwt_decode + +from social.exceptions import AuthTokenError from social.backends.oauth import BaseOAuth2 -import requests -import time -import urllib + class AzureADOAuth2(BaseOAuth2): name = 'azuread-oauth2' @@ -79,12 +76,10 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): response = kwargs.get('response') id_token = response.get('id_token') - try: decoded_id_token = jwt_decode(id_token, verify=False) except (DecodeError, ExpiredSignature) as de: raise AuthTokenError(self, de) - return decoded_id_token def auth_extra_arguments(self): @@ -92,18 +87,15 @@ def auth_extra_arguments(self): overriden by GET parameters.""" extra_arguments = {} resource = self.setting('RESOURCE') - if resource: - extra_arguments = { - 'resource': resource - } - + extra_arguments = {'resource': resource} return extra_arguments def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" - data = super(AzureADOAuth2, self).extra_data(user, uid, response,details, *args, **kwargs) + data = super(AzureADOAuth2, self).extra_data(user, uid, response, + details, *args, **kwargs) data['resource'] = self.setting('RESOURCE') return data @@ -115,16 +107,12 @@ def refresh_token_params(self, token, *args, **kwargs): } def get_auth_token(self, user_id): - """Return the access token for the given user, after ensuring that it has not expired, - or refreshing it if so.""" + """Return the access token for the given user, after ensuring that it + has not expired, or refreshing it if so.""" user = self.get_user(user_id=user_id) - access_token = user.social_user.access_token expires_on = user.social_user.extra_data['expires_on'] - if expires_on <= int(time.time()): new_token_response = self.refresh_token(token=access_token) access_token = new_token_response['access_token'] - return access_token - From 85a8fb71e028a0f3bf67ec980db2451008fcd0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 13:28:23 -0300 Subject: [PATCH 607/890] PEP8 --- social/tests/backends/test_azuread.py | 45 ++++++++++----------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/social/tests/backends/test_azuread.py b/social/tests/backends/test_azuread.py index e2e36a4c5..3bfa6829a 100644 --- a/social/tests/backends/test_azuread.py +++ b/social/tests/backends/test_azuread.py @@ -1,7 +1,7 @@ """ Copyright (c) 2015 Microsoft Open Technologies, Inc. -All rights reserved. +All rights reserved. MIT License @@ -25,33 +25,35 @@ """ import json -from social.p3 import urlencode + from social.tests.backends.oauth import OAuth2Test -from httpretty import HTTPretty -from social.tests.models import User -class AzureADOAuth2Test(OAuth2Test): +class AzureADOAuth2Test(OAuth2Test): backend_path = 'social.backends.azuread.AzureADOAuth2' user_data_url = 'https://graph.windows.net/me' expected_username = 'foobar' - access_token_body = json.dumps({ 'access_token': 'foobar', 'token_type': 'bearer', - 'id_token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83Mjc0MDZhYy03MDY4' - 'LTQ4ZmEtOTJiOS1jMmQ2NzIxMWJjNTAvIiwiaWF0IjpudWxsLCJleHAiOm51bGwsImF1ZCI6IjAyOWNjMDEwLWJiNzQtNGQyY' - 'i1hMDQwLWY5Y2VkM2ZkMmM3NiIsInN1YiI6InFVOHhrczltSHFuVjZRMzR6aDdTQVpvY2loOUV6cnJJOW1wVlhPSWJWQTgiLC' - 'J2ZXIiOiIxLjAiLCJ0aWQiOiI3Mjc0MDZhYy03MDY4LTQ4ZmEtOTJiOS1jMmQ2NzIxMWJjNTAiLCJvaWQiOiI3ZjhlMTk2OS0' - '4YjgxLTQzOGMtOGQ0ZS1hZDZmNTYyYjI4YmIiLCJ1cG4iOiJmb29iYXJAdGVzdC5vbm1pY3Jvc29mdC5jb20iLCJnaXZlbl9u' - 'YW1lIjoiZm9vIiwiZmFtaWx5X25hbWUiOiJiYXIiLCJuYW1lIjoiZm9vIGJhciIsInVuaXF1ZV9uYW1lIjoiZm9vYmFyQHRlc' - '3Qub25taWNyb3NvZnQuY29tIiwicHdkX2V4cCI6IjQ3MzMwOTY4IiwicHdkX3VybCI6Imh0dHBzOi8vcG9ydGFsLm1pY3Jvc2' - '9mdG9ubGluZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn2g7BjX5JxNe8skYlK4PdhiLz4', + 'id_token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL' + '3N0cy53aW5kb3dzLm5ldC83Mjc0MDZhYy03MDY4LTQ4ZmEtOTJiOS1jMmQ' + '2NzIxMWJjNTAvIiwiaWF0IjpudWxsLCJleHAiOm51bGwsImF1ZCI6IjAyO' + 'WNjMDEwLWJiNzQtNGQyYi1hMDQwLWY5Y2VkM2ZkMmM3NiIsInN1YiI6In' + 'FVOHhrczltSHFuVjZRMzR6aDdTQVpvY2loOUV6cnJJOW1wVlhPSWJWQTg' + 'iLCJ2ZXIiOiIxLjAiLCJ0aWQiOiI3Mjc0MDZhYy03MDY4LTQ4ZmEtOTJi' + 'OS1jMmQ2NzIxMWJjNTAiLCJvaWQiOiI3ZjhlMTk2OS04YjgxLTQzOGMtO' + 'GQ0ZS1hZDZmNTYyYjI4YmIiLCJ1cG4iOiJmb29iYXJAdGVzdC5vbm1pY3' + 'Jvc29mdC5jb20iLCJnaXZlbl9uYW1lIjoiZm9vIiwiZmFtaWx5X25hbWU' + 'iOiJiYXIiLCJuYW1lIjoiZm9vIGJhciIsInVuaXF1ZV9uYW1lIjoiZm9v' + 'YmFyQHRlc3Qub25taWNyb3NvZnQuY29tIiwicHdkX2V4cCI6IjQ3MzMwO' + 'TY4IiwicHdkX3VybCI6Imh0dHBzOi8vcG9ydGFsLm1pY3Jvc29mdG9ubG' + 'luZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn' + '2g7BjX5JxNe8skYlK4PdhiLz4', 'expires_in': 3600, 'expires_on': 1423650396, 'not_before': 1423646496 }) - refresh_token_body = json.dumps({ 'access_token': 'foobar-new-token', 'token_type': 'bearer', @@ -69,16 +71,3 @@ def test_partial_pipeline(self): def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(social.extra_data['access_token'], 'foobar-new-token') - - # TODO: - # def test_get_access_token(self): - # self.do_login() - # HTTPretty.register_uri(self._method(self.backend.REFRESH_TOKEN_METHOD), - # self.backend.REFRESH_TOKEN_URL or - # self.backend.ACCESS_TOKEN_URL, - # status=200, - # body=self.refresh_token_body) - # user = list(User.cache.values())[0] - # token = self.backend.get_auth_token(user_id=user.id) - # self.assertEqual(token, 'foobar-new-token') - \ No newline at end of file From 278945f82d19588c4a6c7e9a95ff1bafe944036c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 18:01:05 -0300 Subject: [PATCH 608/890] Coding style --- social/backends/weixin.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/social/backends/weixin.py b/social/backends/weixin.py index 5170cafd2..b1308f313 100644 --- a/social/backends/weixin.py +++ b/social/backends/weixin.py @@ -30,20 +30,22 @@ def get_user_details(self, response): username = response.get('domain', '') else: username = response.get('nickname', '') - profile_image_url = response.get('headimgurl', '') - return {'username': username, 'profile_image_url': profile_image_url} + return { + 'username': username, + 'profile_image_url': response.get('headimgurl', '') + } def user_data(self, access_token, *args, **kwargs): - data = self.get_json('https://api.weixin.qq.com/sns/userinfo', - params={'access_token': access_token, - 'openid': kwargs['response']['openid']}) + data = self.get_json('https://api.weixin.qq.com/sns/userinfo', params={ + 'access_token': access_token, + 'openid': kwargs['response']['openid'] + }) nickname = data.get('nickname') if nickname: # weixin api has some encode bug, here need handle data['nickname'] = nickname.encode('raw_unicode_escape').decode('utf-8') return data - def auth_params(self, state=None): appid, secret = self.get_key_and_secret() params = { From 863a042b05b6cb3b1f64c745650891f4beba4d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 18:06:34 -0300 Subject: [PATCH 609/890] Fix changetip docs errors --- docs/backends/changetip.rst | 4 ++-- docs/backends/index.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/backends/changetip.rst b/docs/backends/changetip.rst index dcc9cc502..44f97c28b 100644 --- a/docs/backends/changetip.rst +++ b/docs/backends/changetip.rst @@ -1,5 +1,5 @@ ChangeTip -===== +========= ChangeTip @@ -18,5 +18,5 @@ ChangeTip See auth scopes at `ChangeTip OAuth docs`_. - .. _ChangeTip: https://www.changetip.com/api +.. _ChangeTip OAuth docs: https://www.changetip.com/api/auth/#!#scopes diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 81d031101..0c16cd5c2 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -57,6 +57,7 @@ Social backends belgium_eid bitbucket box + changetip clef coinbase coursera From c2dee681b57b200c9a2f83c8920fd7a0c6991c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 18:08:59 -0300 Subject: [PATCH 610/890] Newline at end of file --- docs/backends/digitalocean.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/digitalocean.rst b/docs/backends/digitalocean.rst index f50f41324..5f72cfe02 100644 --- a/docs/backends/digitalocean.rst +++ b/docs/backends/digitalocean.rst @@ -21,4 +21,4 @@ developer's documentation`_ for more information. .. _DigitalOcean developer's documentation: https://developers.digitalocean.com/documentation/ -.. _Apps & API page: https://cloud.digitalocean.com/settings/applications \ No newline at end of file +.. _Apps & API page: https://cloud.digitalocean.com/settings/applications From 6b1e301c7960827fbeeff13d580f790b297cf791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 May 2015 21:03:01 -0300 Subject: [PATCH 611/890] v0.2.10 --- Changelog | 76 ++++++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index cf8a91e01..01e799d13 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,82 @@ +2015-05-29 v0.2.10 +================== + + * 2015-05-29 Matías Aguirre + Newline at end of file + + * 2015-05-29 Matías Aguirre + Fix changetip docs errors + + * 2015-05-29 Matías Aguirre + Coding style + + * 2015-05-29 Matías Aguirre + PEP8 + + * 2015-05-29 Matías Aguirre + PEP8 + + * 2015-05-29 Matías Aguirre + Avoid storing empty values from user details + + * 2015-05-26 vinhub + Added Azure AD docs to index.rst + + * 2015-05-26 Vinayak (Vin) Bhalerao + Removed + + * 2015-05-25 sushantgawali + changes in extra_data + + * 2015-05-18 sushantgawali + added copyright / license notices + + * 2015-05-15 sushantgawali + updated test cases + + * 2015-05-13 sushantgawali + added get_auth_token method for ease of use + + * 2015-05-12 sushantgawali + added test cases for AzureADOAuth2 refresh token method added + + * 2015-05-11 sushantgawali + added generic resource setting added license text added documentation file + + * 2015-05-11 sushantgawali + cleaned up unneeded Sharepoint related code + + * 2015-05-11 sushantgawali + Added provider for Microsoft Azure Active Directory OAuth2 + + * 2015-05-20 Marek Jalovec + Fixes "ImportError: No module named packages.urllib3.poolmanager" error + (fixes #617) + + * 2015-05-20 blurrcat + fix Fitbit OAuth 1 authorization URL + + * 2015-05-17 duoduo369 + add weixin backends + + * 2015-05-16 Andrew Starr-Bochicchio + Add a DigitalOcean backend. + + * 2015-05-08 Matías Aguirre + Ensure that all the requirements are installed + + * 2015-05-08 Matías Aguirre + Fix syntax (backward compatible) + + * 2015-05-07 Matías Aguirre + Dev version flagged + 2015-05-07 v0.2.9 ================= + * 2015-05-07 Matías Aguirre + v0.2.9 + * 2015-05-07 Matías Aguirre Fix manifest definition diff --git a/social/__init__.py b/social/__init__.py index 365327065..135e46aad 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -3,5 +3,5 @@ registration/authentication just adding a few configurations. """ version = (0, 2, 10) -extra = '-dev' +extra = '' __version__ = '.'.join(map(str, version)) + extra From 9fe591ca1c1a45e77be5c382ff5f0b6d93fda64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bompard?= Date: Mon, 1 Jun 2015 13:05:40 +0200 Subject: [PATCH 612/890] Keep the egg-info directory in the sdist Fixes #623 --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2cf5b4839..d2fef433d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,6 @@ recursive-include social/tests *.txt graft examples recursive-exclude .tox * -recursive-exclude python_social_auth.egg-info * recursive-exclude social *.pyc recursive-exclude examples *.pyc recursive-exclude examples *.db From d064794204a424a28fc7ab6f6964dba40e573ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 17 Jun 2015 02:51:19 -0300 Subject: [PATCH 613/890] PEP8 --- social/backends/saml.py | 218 ++++++++++++++++----------- social/strategies/base.py | 10 +- social/strategies/django_strategy.py | 10 +- social/tests/backends/test_saml.py | 89 ++++++----- social/tests/models.py | 6 +- 5 files changed, 193 insertions(+), 140 deletions(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index 0ea11c07a..745451b23 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -4,10 +4,12 @@ Terminology: "Service Provider" (SP): Your web app -"Identity Provider" (IdP): The third-party site that is authenticating users via SAML +"Identity Provider" (IdP): The third-party site that is authenticating + users via SAML """ from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.settings import OneLogin_Saml2_Settings + from social.backends.base import BaseAuth from social.exceptions import AuthFailed @@ -22,88 +24,104 @@ class SAMLIdentityProvider(object): - """ - Wrapper around configuration for a SAML Identity provider - """ - + """Wrapper around configuration for a SAML Identity provider""" def __init__(self, name, **kwargs): - """ Load and parse configuration """ + """Load and parse configuration""" self.name = name - # name should be a slug and must not contain a colon, which could conflict with uid prefixing: - assert ':' not in self.name and ' ' not in self.name, "IdP 'name' should be a slug (short, no spaces)" + # name should be a slug and must not contain a colon, which + # could conflict with uid prefixing: + assert ':' not in self.name and ' ' not in self.name, \ + 'IdP "name" should be a slug (short, no spaces)' self.conf = kwargs def get_user_permanent_id(self, attributes): """ - The most important method: Get a permanent, unique identifier for this user from the - attributes supplied by the IdP. + The most important method: Get a permanent, unique identifier + for this user from the attributes supplied by the IdP. - If you want to use the NameID, it's available via attributes['name_id'] + If you want to use the NameID, it's available via + attributes['name_id'] """ - return attributes[self.conf.get('attr_user_permanent_id', OID_USERID)][0] + return attributes[ + self.conf.get('attr_user_permanent_id', OID_USERID) + ][0] # Attributes processing: def get_user_details(self, attributes): """ - Given the SAML attributes extracted from the SSO response, get the user data like name. + Given the SAML attributes extracted from the SSO response, get + the user data like name. """ return { - 'fullname': self.get_attr(attributes, 'attr_full_name', OID_COMMON_NAME), - 'first_name': self.get_attr(attributes, 'attr_first_name', OID_GIVEN_NAME), - 'last_name': self.get_attr(attributes, 'attr_last_name', OID_SURNAME), - 'username': self.get_attr(attributes, 'attr_username', OID_USERID), - 'email': self.get_attr(attributes, 'attr_email', OID_MAIL), + 'fullname': self.get_attr(attributes, 'attr_full_name', + OID_COMMON_NAME), + 'first_name': self.get_attr(attributes, 'attr_first_name', + OID_GIVEN_NAME), + 'last_name': self.get_attr(attributes, 'attr_last_name', + OID_SURNAME), + 'username': self.get_attr(attributes, 'attr_username', + OID_USERID), + 'email': self.get_attr(attributes, 'attr_email', + OID_MAIL), } def get_attr(self, attributes, conf_key, default_attribute): """ Internal helper method. - Get the attribute 'default_attribute' out of the attributes, unless self.conf[conf_key] - overrides the default by specifying another attribute to use. + Get the attribute 'default_attribute' out of the attributes, + unless self.conf[conf_key] overrides the default by specifying + another attribute to use. """ key = self.conf.get(conf_key, default_attribute) return attributes[key][0] if key in attributes else None @property def entity_id(self): - """ Get the entity ID for this IdP """ - return self.conf['entity_id'] # Required. e.g. "https://idp.testshib.org/idp/shibboleth" + """Get the entity ID for this IdP""" + # Required. e.g. "https://idp.testshib.org/idp/shibboleth" + return self.conf['entity_id'] @property def sso_url(self): - """ Get the SSO URL for this IdP """ - return self.conf['url'] # Required. e.g. "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" + """Get the SSO URL for this IdP""" + # Required. e.g. + # "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" + return self.conf['url'] @property def x509cert(self): - """ X.509 Public Key Certificate for this IdP """ + """X.509 Public Key Certificate for this IdP""" return self.conf['x509cert'] @property def saml_config_dict(self): - """ Get the IdP configuration dict in the format required by python-saml """ + """Get the IdP configuration dict in the format required by + python-saml""" return { - "entityId": self.entity_id, - "singleSignOnService": { - "url": self.sso_url, - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", # python-saml only supports Redirect + 'entityId': self.entity_id, + 'singleSignOnService': { + 'url': self.sso_url, + # python-saml only supports Redirect + 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' }, - "x509cert": self.x509cert, + 'x509cert': self.x509cert, } class DummySAMLIdentityProvider(SAMLIdentityProvider): """ - A placeholder IdP used when we must specify something, e.g. when generating SP metadata. + A placeholder IdP used when we must specify something, e.g. when + generating SP metadata. - If OneLogin_Saml2_Auth is modified to not always require IdP config, this can be removed. + If OneLogin_Saml2_Auth is modified to not always require IdP + config, this can be removed. """ def __init__(self): super(DummySAMLIdentityProvider, self).__init__( - "dummy", - entity_id="https://dummy.none/saml2", - url="https://dummy.none/SSO", - x509cert='', + 'dummy', + entity_id='https://dummy.none/saml2', + url='https://dummy.none/SSO', + x509cert='' ) @@ -123,15 +141,26 @@ class SAMLAuth(BaseAuth): SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = "... X.509 certificate string ..." SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = "... private key ..." SOCIAL_AUTH_SAML_ORG_INFO = { - "en-US": {"name": "example", "displayname": "Example Inc.", "url": "http://example.com", }, + "en-US": { + "name": "example", + "displayname": "Example Inc.", + "url": "http://example.com" + } + } + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = { + "givenName": "Tech Gal", + "emailAddress": "technical@example.com" + } + SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { + "givenName": "Support Guy", + "emailAddress": "support@example.com" } - SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {"givenName": "Tech Gal", "emailAddress": "technical@example.com", } - SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {"givenName": "Support Guy", "emailAddress": "support@example.com", } SOCIAL_AUTH_SAML_ENABLED_IDPS = { "testshib": { "entity_id": "https://idp.testshib.org/idp/shibboleth", "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", - "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0B ... 8Bbnl+ev0peYzxFyF5sQA==", + "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0B... + ...8Bbnl+ev0peYzxFyF5sQA==", } } @@ -143,40 +172,41 @@ class SAMLAuth(BaseAuth): name = "saml" def get_idp(self, idp_name): - """ Given the name of an IdP, get a SAMLIdentityProvider instance """ - idp_config = self.setting("ENABLED_IDPS")[idp_name] + """Given the name of an IdP, get a SAMLIdentityProvider instance""" + idp_config = self.setting('ENABLED_IDPS')[idp_name] return SAMLIdentityProvider(idp_name, **idp_config) def generate_saml_config(self, idp): """ Generate the configuration required to instantiate OneLogin_Saml2_Auth """ - # The shared absolute URL that all IdPs redirect back to - this is specified in our metadata.xml: + # The shared absolute URL that all IdPs redirect back to - + # this is specified in our metadata.xml: abs_completion_url = self.redirect_uri - config = { - "contactPerson": { - "technical": self.setting("TECHNICAL_CONTACT"), - "support": self.setting("SUPPORT_CONTACT"), + 'contactPerson': { + 'technical': self.setting('TECHNICAL_CONTACT'), + 'support': self.setting('SUPPORT_CONTACT') }, - "debug": True, - "idp": idp.saml_config_dict, - "organization": self.setting("ORG_INFO"), - "security": { + 'debug': True, + 'idp': idp.saml_config_dict, + 'organization': self.setting('ORG_INFO'), + 'security': { 'metadataValidUntil': '', 'metadataCacheDuration': 'P10D', # metadata valid for ten days }, - "sp": { - "assertionConsumerService": { - "url": abs_completion_url, - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", # python-saml only supports HTTP-POST + 'sp': { + 'assertionConsumerService': { + 'url': abs_completion_url, + # python-saml only supports HTTP-POST + 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' }, - "entityId": self.setting("SP_ENTITY_ID"), - "NameIDFormats": self.setting("SP_NAMEID_FORMATS", []), - "x509cert": self.setting("SP_PUBLIC_CERT"), - "privateKey": self.setting("SP_PRIVATE_KEY"), + 'entityId': self.setting('SP_ENTITY_ID'), + 'NameIDFormats': self.setting('SP_NAMEID_FORMATS', []), + 'x509cert': self.setting('SP_PUBLIC_CERT'), + 'privateKey': self.setting('SP_PRIVATE_KEY'), }, - "strict": True, # We must force strict mode - for security + 'strict': True, # We must force strict mode - for security } config["security"].update(self.setting("SECURITY_CONFIG", {})) config["sp"].update(self.setting("SP_EXTRA", {})) @@ -184,22 +214,28 @@ def generate_saml_config(self, idp): def generate_metadata_xml(self): """ - Helper method that can be used from your web app to generate the XML metadata required - to link your web app as a Service Provider with each IdP you wish to use. + Helper method that can be used from your web app to generate the XML + metadata required to link your web app as a Service Provider with + each IdP you wish to use. Returns (metadata XML string, list of errors) Example usage (Django): - from social.apps.django_app.utils import load_strategy, load_backend + from social.apps.django_app.utils import load_strategy, \ + load_backend def saml_metadata_view(request): complete_url = reverse('social:complete', args=("saml", )) - saml_backend = load_backend(load_strategy(request), "saml", complete_url) + saml_backend = load_backend(load_strategy(request), "saml", + complete_url) metadata, errors = saml_backend.generate_metadata_xml() if not errors: - return HttpResponse(content=metadata, content_type='text/xml') + return HttpResponse(content=metadata, + content_type='text/xml') return HttpResponseServerError(content=', '.join(errors)) """ - idp = DummySAMLIdentityProvider() # python-saml requires us to specify something here even though it's not used + # python-saml requires us to specify something here even + # though it's not used + idp = DummySAMLIdentityProvider() config = self.generate_saml_config(idp) saml_settings = OneLogin_Saml2_Settings(config) metadata = saml_settings.get_sp_metadata() @@ -207,9 +243,7 @@ def saml_metadata_view(request): return metadata, errors def _create_saml_auth(self, idp): - """ - Get an instance of OneLogin_Saml2_Auth - """ + """Get an instance of OneLogin_Saml2_Auth""" config = self.generate_saml_config(idp) request_info = { 'https': 'on' if self.strategy.request_is_secure() else 'off', @@ -222,26 +256,27 @@ def _create_saml_auth(self, idp): return OneLogin_Saml2_Auth(request_info, config) def auth_url(self): - """ Get the URL to which we must redirect in order to authenticate the user """ + """Get the URL to which we must redirect in order to + authenticate the user""" idp_name = self.strategy.request_data()['idp'] auth = self._create_saml_auth(idp=self.get_idp(idp_name)) - # Below, return_to sets the RelayState, which can contain arbitrary data. - # We use it to store the specific SAML IdP name, since we multiple IdPs - # share the same auth_complete URL. + # Below, return_to sets the RelayState, which can contain + # arbitrary data. We use it to store the specific SAML IdP + # name, since we multiple IdPs share the same auth_complete + # URL. return auth.login(return_to=idp_name) def get_user_details(self, response): - """ - Get user details like full name, email, etc. from the response - see auth_complete - """ + """Get user details like full name, email, etc. from the + response - see auth_complete""" idp = self.get_idp(response['idp_name']) return idp.get_user_details(response['attributes']) def get_user_id(self, details, response): """ Get the permanent ID for this user from the response. - We prefix each ID with the name of the IdP so that we can connect multiple IdPs to this - user. + We prefix each ID with the name of the IdP so that we can + connect multiple IdPs to this user. """ idp = self.get_idp(response['idp_name']) uid = idp.get_user_permanent_id(response['attributes']) @@ -249,8 +284,8 @@ def get_user_id(self, details, response): def auth_complete(self, *args, **kwargs): """ - The user has been redirected back from the IdP and we should now log them in, if - everything checks out. + The user has been redirected back from the IdP and we should + now log them in, if everything checks out. """ idp_name = self.strategy.request_data()['RelayState'] idp = self.get_idp(idp_name) @@ -259,31 +294,32 @@ def auth_complete(self, *args, **kwargs): errors = auth.get_errors() if errors or not auth.is_authenticated(): reason = auth.get_last_error_reason() - raise AuthFailed(self, 'SAML login failed: {} ({})'.format(errors, reason)) + raise AuthFailed( + self, 'SAML login failed: {} ({})'.format(errors, reason) + ) attributes = auth.get_attributes() attributes['name_id'] = auth.get_nameid() - self._check_entitlements(idp, attributes) - response = { 'idp_name': idp_name, 'attributes': attributes, 'session_index': auth.get_session_index(), } - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) def _check_entitlements(self, idp, attributes): """ - Additional verification of a SAML response before authenticating the user. + Additional verification of a SAML response before + authenticating the user. - Subclasses can override this method if they need custom validation code, - such as requiring the presence of an eduPersonEntitlement. + Subclasses can override this method if they need custom + validation code, such as requiring the presence of an + eduPersonEntitlement. - raise social.exceptions.AuthForbidden if the user should not be authenticated, - or do nothing to allow the login pipeline to continue. + raise social.exceptions.AuthForbidden if the user should not + be authenticated, or do nothing to allow the login pipeline to + continue. """ pass diff --git a/social/strategies/base.py b/social/strategies/base.py index 09ef9fa27..ce66af09e 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -190,21 +190,21 @@ def build_absolute_uri(self, path=None): raise NotImplementedError('Implement in subclass') def request_is_secure(self): - """ Is the request using HTTPS? """ + """Is the request using HTTPS?""" raise NotImplementedError('Implement in subclass') def request_path(self): - """ path of the current request """ + """path of the current request""" raise NotImplementedError('Implement in subclass') def request_port(self): - """ Port in use for this request """ + """Port in use for this request""" raise NotImplementedError('Implement in subclass') def request_get(self): - """ Request GET data """ + """Request GET data""" raise NotImplementedError('Implement in subclass') def request_post(self): - """ Request POST data """ + """Request POST data""" raise NotImplementedError('Implement in subclass') diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index b3b66b791..2cfc82842 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -54,23 +54,23 @@ def request_host(self): return self.request.get_host() def request_is_secure(self): - """ Is the request using HTTPS? """ + """Is the request using HTTPS?""" return self.request.is_secure() def request_path(self): - """ path of the current request """ + """path of the current request""" return self.request.path def request_port(self): - """ Port in use for this request """ + """Port in use for this request""" return self.request.META['SERVER_PORT'] def request_get(self): - """ Request GET data """ + """Request GET data""" return self.request.GET.copy() def request_post(self): - """ Request POST data """ + """Request POST data""" return self.request.POST.copy() def redirect(self, url): diff --git a/social/tests/backends/test_saml.py b/social/tests/backends/test_saml.py index abe256976..057fe18e7 100644 --- a/social/tests/backends/test_saml.py +++ b/social/tests/backends/test_saml.py @@ -1,89 +1,99 @@ -import base64 -import datetime -from httpretty import HTTPretty +import re import json +import sys +import unittest2 +import os.path +import requests + from mock import patch +from httpretty import HTTPretty + try: from onelogin.saml2.utils import OneLogin_Saml2_Utils except ImportError: - pass # Only available for python 2.7 at the moment, so don't worry if this fails -import os.path -import re -import requests -from social.p3 import urlparse -from social.utils import parse_qs, url_add_parameters -from social.tests.models import User + # Only available for python 2.7 at the moment, so don't worry if this fails + pass + +from social.utils import parse_qs from social.tests.backends.base import BaseBackendTest -import sys -import unittest2 -try: - from urllib.parse import urlencode, urlparse, urlunparse, parse_qs -except ImportError: - from urllib import urlencode - from urlparse import urlparse, urlunparse, parse_qs +from social.p3 import urlparse, urlunparse, urlencode + DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') @unittest2.skipUnless( sys.version_info[:2] == (2, 7), - "python-saml currently depends on 2.7; 3+ support coming soon") -@unittest2.skipIf('__pypy__' in sys.builtin_module_names, "dm.xmlsec not compatible with pypy") + 'python-saml currently depends on 2.7; 3+ support coming soon') +@unittest2.skipIf('__pypy__' in sys.builtin_module_names, + 'dm.xmlsec not compatible with pypy') class SAMLTest(BaseBackendTest): backend_path = 'social.backends.saml.SAMLAuth' expected_username = 'myself' def extra_settings(self): - with open(os.path.join(DATA_DIR, 'saml_config.json'), 'r') as config_file: + name = os.path.join(DATA_DIR, 'saml_config.json') + with open(name, 'r') as config_file: config_str = config_file.read() return json.loads(config_str) def setUp(self): - """ Patch the time so that we can replay canned request/response pairs """ + """Patch the time so that we can replay canned + request/response pairs""" super(SAMLTest, self).setUp() @staticmethod def fixed_time(): - return OneLogin_Saml2_Utils.parse_SAML_to_time("2015-05-09T03:57:22Z") + return OneLogin_Saml2_Utils.parse_SAML_to_time( + '2015-05-09T03:57:22Z' + ) now_patch = patch.object(OneLogin_Saml2_Utils, 'now', fixed_time) now_patch.start() self.addCleanup(now_patch.stop) def install_http_intercepts(self, start_url, return_url): - # When we request start_url (https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO...) - # we will eventually get a redirect back, with SAML assertion data in the query string. - # A pre-recorded correct response is kept in this .txt file: - with open(os.path.join(DATA_DIR, 'saml_response.txt'), 'r') as response_file: + # When we request start_url + # (https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO...) + # we will eventually get a redirect back, with SAML assertion + # data in the query string. A pre-recorded correct response + # is kept in this .txt file: + name = os.path.join(DATA_DIR, 'saml_response.txt') + with open(name, 'r') as response_file: response_url = response_file.read() - HTTPretty.register_uri(HTTPretty.GET, start_url, status=301, location=response_url) - HTTPretty.register_uri(HTTPretty.GET, return_url, status=200, body='foobar') + HTTPretty.register_uri(HTTPretty.GET, start_url, status=301, + location=response_url) + HTTPretty.register_uri(HTTPretty.GET, return_url, status=200, + body='foobar') def do_start(self): # pretend we've started with a URL like /login/saml/?idp=testshib: self.strategy.set_request_data({'idp': 'testshib'}, self.backend) start_url = self.backend.start().url - # Modify the start URL to make the SAML request consistent from test to test: + # Modify the start URL to make the SAML request consistent + # from test to test: start_url = self.modify_start_url(start_url) - # If the SAML Identity Provider recognizes the user, we will be redirected back to: + # If the SAML Identity Provider recognizes the user, we will + # be redirected back to: return_url = self.backend.redirect_uri self.install_http_intercepts(start_url, return_url) response = requests.get(start_url) self.assertTrue(response.url.startswith(return_url)) self.assertEqual(response.text, 'foobar') - query_values = dict((k, v[0]) for k, v in parse_qs(urlparse(response.url).query).items()) + query_values = dict((k, v[0]) for k, v in + parse_qs(urlparse(response.url).query).items()) self.assertNotIn(' ', query_values['SAMLResponse']) self.strategy.set_request_data(query_values, self.backend) return self.backend.complete() def test_metadata_generation(self): - """ Test that we can generate the metadata without error """ + """Test that we can generate the metadata without error""" xml, errors = self.backend.generate_metadata_xml() self.assertEqual(len(errors), 0) self.assertEqual(xml[0], '<') def test_login(self): - """ Test that we can authenticate with a SAML IdP (TestShib) """ - user = self.do_login() + """Test that we can authenticate with a SAML IdP (TestShib)""" + self.do_login() def modify_start_url(self, start_url): """ @@ -92,13 +102,18 @@ def modify_start_url(self, start_url): """ # Parse the SAML Request URL to get the XML being sent to TestShib url_parts = urlparse(start_url) - query = dict((k, v[0]) for (k, v) in parse_qs(url_parts.query).iteritems()) - xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(query['SAMLRequest']) + query = dict((k, v[0]) for (k, v) in + parse_qs(url_parts.query).iteritems()) + xml = OneLogin_Saml2_Utils.decode_base64_and_inflate( + query['SAMLRequest'] + ) # Modify the XML: xml, changed = re.subn(r'ID="[^"]+"', 'ID="TEST_ID"', xml) self.assertEqual(changed, 1) # Update the URL to use the modified query string: - query['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) + query['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode( + xml + ) url_parts = list(url_parts) url_parts[4] = urlencode(query) return urlunparse(url_parts) diff --git a/social/tests/models.py b/social/tests/models.py index 80bf6871e..d5d193c2c 100644 --- a/social/tests/models.py +++ b/social/tests/models.py @@ -1,7 +1,7 @@ import base64 from social.storage.base import UserMixin, NonceMixin, AssociationMixin, \ - CodeMixin, BaseStorage + CodeMixin, BaseStorage class BaseModel(object): @@ -117,7 +117,9 @@ def get_social_auth(cls, provider, uid): @classmethod def get_social_auth_for_user(cls, user, provider=None, id=None): - return [usa for usa in user.social if provider in (None, usa.provider) and id in (None, usa.id)] + return [usa for usa in user.social + if provider in (None, usa.provider) and + id in (None, usa.id)] @classmethod def create_social_auth(cls, user, uid, provider): From b567c0ee30ad860fe436a5419326aee4df5d3771 Mon Sep 17 00:00:00 2001 From: Maarten van Schaik Date: Wed, 17 Jun 2015 14:54:52 +0200 Subject: [PATCH 614/890] Python3 fixes for Tornado --- social/apps/tornado_app/routes.py | 2 +- social/strategies/tornado_strategy.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/social/apps/tornado_app/routes.py b/social/apps/tornado_app/routes.py index a9ff01385..30b78395b 100644 --- a/social/apps/tornado_app/routes.py +++ b/social/apps/tornado_app/routes.py @@ -1,6 +1,6 @@ from tornado.web import url -from handlers import AuthHandler, CompleteHandler, DisconnectHandler +from .handlers import AuthHandler, CompleteHandler, DisconnectHandler SOCIAL_AUTH_ROUTES = [ diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index 7e3f11252..f10a885f1 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -1,4 +1,5 @@ import json +import six from tornado.template import Loader, Template @@ -29,7 +30,7 @@ def get_setting(self, name): def request_data(self, merge=True): # Multiple valued arguments not supported yet return dict((key, val[0]) - for key, val in self.request.arguments.iteritems()) + for key, val in six.iteritems(self.request.arguments)) def request_host(self): return self.request.host From 3a6be39a32874d032337fb7cd50f27c5cafcec17 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Thu, 18 Jun 2015 11:59:06 -0700 Subject: [PATCH 615/890] Added documentation --- docs/backends/saml.rst | 171 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 docs/backends/saml.rst diff --git a/docs/backends/saml.rst b/docs/backends/saml.rst new file mode 100644 index 000000000..72de2fd0d --- /dev/null +++ b/docs/backends/saml.rst @@ -0,0 +1,171 @@ +SAML +==== + +The SAML backend allows users to authenticate with any provider that supports +the SAML 2.0 protocol (commonly used for corporate or academic single sign on). + +The SAML backend for python-social-auth allows your web app to act as a SAML +Service Provider. You can configure one or more SAML Identity Providers that +users can use for authentication. For example, if your users are students, you +could enable Harvard and MIT as identity providers, so that students of either +of those two universities can use their campus login to access your app. + +Required Configuration +---------------------- + +At a minimum, you must add the following to your project's settings: + +- ``SOCIAL_AUTH_SAML_SP_ENTITY_ID``: The SAML Entity ID for your app. This + should be a URL that includes a domain name you own. It doesn't matter what + the URL points to. Example: ``http://saml.yoursite.com`` + +- ``SOCIAL_AUTH_SAML_SP_PUBLIC_CERT``: The X.509 certificate string for the + key pair that your app will use. You can generate a new self-signed key pair + with:: + + openssl req -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.key + + The contents of ``saml.crt`` should then be used as the value of this setting + (you can omit the first and last lines, which aren't required). + +- ``SOCIAL_AUTH_SAML_SP_PRIVATE_KEY``: The private key to be used by your app. + If you used the example openssl command given above, set this to the contents + of ``saml.key`` (again, you can omit the first and last lines). + +- ``SOCIAL_AUTH_SAML_ORG_INFO``: A dictionary that contains information about + your app. You must specify values for English at a minimum. Each language's + entry should specify a ``name`` (not shown to the user), a ``displayname`` + (shown to the user), and a URL. See the following + example:: + + { + "en-US": { + "name": "example", + "displayname": "Example Inc.", + "url": "http://example.com", + } + } + +- ``SOCIAL_AUTH_SAML_TECHNICAL_CONTACT``: A dictionary with two values, + ``givenName`` and ``emailAddress``, describing the name and email of a + technical contact responsible for your app. Example:: + + {"givenName": "Tech Gal", "emailAddress": "technical@example.com"} + +- ``SOCIAL_AUTH_SAML_TECHNICAL_CONTACT``: A dictionary with two values, + ``givenName`` and ``emailAddress``, describing the name and email of a + support contact for your app. Example:: + + SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { + "givenName": "Support Guy", + "emailAddress": "support@example.com", + } + +- ``SOCIAL_AUTH_SAML_ENABLED_IDPS``: The most important setting. List the Entity + ID, SSO URL, and x.509 public key certificate for each provider that your app + wants to support. The SSO URL must support the ``HTTP-Redirect`` binding. + You can get these values from the provider's XML metadata. Here's an example, + for TestShib_ (the values come from TestShib's metadata_):: + + { + "testshib": { + "entity_id": "https://idp.testshib.org/idp/shibboleth", + "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", + "x509cert": "MIIEDjCCAvagAwIBAgIBADA ... 8Bbnl+ev0peYzxFyF5sQA==", + } + } + +Basic Usage +----------- + +- Set all of the required configuration variables described above. + +- Generate the SAML XML metadata for your app. The best way to do this is to + create a new view/page/URL in your app that will call the backend's + ``generate_metadata_xml()`` method. Here's an example of how to do this in + Django:: + + def saml_metadata_view(request): + complete_url = reverse('social:complete', args=("saml", )) + saml_backend = load_backend( + load_strategy(request), + "saml", + redirect_uri=complete_url, + ) + metadata, errors = saml_backend.generate_metadata_xml() + if not errors: + return HttpResponse(content=metadata, content_type='text/xml') + +- Download the metadata for your app that was generated by the above method, + and send it to each Identity Provider (IdP) that you wish to use. Each IdP + must install and configure your metadata on their system before it will work. + +- Now everything is set! To allow users to login with any given IdP, you need to + give them a link to the python-social-auth "begin"/"auth" URL and include an + ``idp`` query parameter that specifies the name of the IdP to use. This is + needed since the backend supports multiple IdPs. The names of the IdPs are the + keys used in the ``SOCIAL_AUTH_SAML_ENABLED_IDPS`` setting. + + Django example:: + + # In view: + context['testshib_url'] = u"{base}?{params}".format( + base=reverse('social:begin', kwargs={'backend': 'saml'}), + params=urllib.urlencode({'next': '/home', 'idp': 'testshib'}) + ) + # In template: + TestShib Login + # Result: + TestShib Login + +- Testing with the TestShib_ provider is recommended, as it is known to work + well. + + +Advanced Settings +----------------- + +- ``SOCIAL_AUTH_SAML_SP_EXTRA``: This can be set to a dict, and any key/value + pairs specified here will be passed to the underlying ``python-saml`` library + configuration's ``sp`` setting. Refer to the ``python-saml`` documentation for + details. + +- ``SOCIAL_AUTH_SAML_SECURITY_CONFIG``: This can be set to a dict, and any + key/value pairs specified here will be passed to the underlying + ``python-saml`` library configuration's ``security`` setting. Two useful keys + that you can set are ``metadataCacheDuration`` and ``metadataValidUntil``, + which control the expiry time of your XML metadata. By default, a cache + duration of 10 days will be used, which means that IdPs are allowed to cache + your metadata for up to 10 days, but no longer. ``metadataCacheDuration`` must + be specified as an ISO 8601 duration string (e.g. `P1D` for one day). + +- ``SOCIAL_AUTH_SAML_SP_NAMEID_FORMATS``: This is a list of ``NameID`` formats + accepted by your app. The default is not to specify any. Example:: + + SOCIAL_AUTH_SAML_SP_NAMEID_FORMATS = [ + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + ] + + +Advanced Usage +-------------- + +You can subclass the ``SAMLAuth`` backend to provide custom functionality. In +particular, there are two methods that are designed for subclasses to override: + +- ``get_idp(self, idp_name)``: Given the name of an IdP, return an instance of + ``SAMLIdentityProvider`` with the details of the IdP. Override this method if + you wish to use some other method for configuring the available identity + providers, such as fetching them at runtime from another server, or using a + list of providers from a Shibboleth federation. + +- ``_check_entitlements(self, idp, attributes)``: This method gets called during + the login process and is where you can decide to accept or reject a user based + on the user's SAML attributes. For example, you can restrict access to your + application to only accept users who belong to a certain department. After + inspecting the passed attributes parameter, do nothing to allow the user to + login, or raise ``social.exceptions.AuthForbidden`` to reject the user. + +.. _TestShib: https://www.testshib.org/ +.. _metadata: https://www.testshib.org/metadata/testshib-providers.xml From 2fdb283d532aeb10e0617ad2ed3be71b4bbe1b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 19 Jun 2015 16:25:57 -0300 Subject: [PATCH 616/890] PEP8 --- social/tests/backends/test_saml.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/social/tests/backends/test_saml.py b/social/tests/backends/test_saml.py index 057fe18e7..2cd552087 100644 --- a/social/tests/backends/test_saml.py +++ b/social/tests/backends/test_saml.py @@ -2,8 +2,8 @@ import json import sys import unittest2 -import os.path import requests +from os import path from mock import patch from httpretty import HTTPretty @@ -14,12 +14,10 @@ # Only available for python 2.7 at the moment, so don't worry if this fails pass -from social.utils import parse_qs from social.tests.backends.base import BaseBackendTest -from social.p3 import urlparse, urlunparse, urlencode +from social.p3 import urlparse, urlunparse, urlencode, parse_qs - -DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +DATA_DIR = path.join(path.dirname(__file__), 'data') @unittest2.skipUnless( @@ -32,7 +30,7 @@ class SAMLTest(BaseBackendTest): expected_username = 'myself' def extra_settings(self): - name = os.path.join(DATA_DIR, 'saml_config.json') + name = path.join(DATA_DIR, 'saml_config.json') with open(name, 'r') as config_file: config_str = config_file.read() return json.loads(config_str) @@ -57,7 +55,7 @@ def install_http_intercepts(self, start_url, return_url): # we will eventually get a redirect back, with SAML assertion # data in the query string. A pre-recorded correct response # is kept in this .txt file: - name = os.path.join(DATA_DIR, 'saml_response.txt') + name = path.join(DATA_DIR, 'saml_response.txt') with open(name, 'r') as response_file: response_url = response_file.read() HTTPretty.register_uri(HTTPretty.GET, start_url, status=301, From 721fd3fe8b71f22f5dcfdd3219557b71fd243766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 19 Jun 2015 16:26:44 -0300 Subject: [PATCH 617/890] Link docs --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 469f1482a..fe9590a09 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -39,6 +39,7 @@ Base OAuth and OpenId classes oauth openid + saml Social backends *************** From 6c59ceb8499af97ec8f4a6cbbdc1f31f76ba6efe Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 21 Jun 2015 08:59:54 -0500 Subject: [PATCH 618/890] Updated Bitbucket backend to use newer 2.0 APIs instead of 1.0 --- social/backends/bitbucket.py | 54 +++++++---------- social/tests/backends/test_bitbucket.py | 79 +++++++++++++++++-------- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index e0d18e2e2..8bb083e7e 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -9,51 +9,41 @@ class BitbucketOAuth(BaseOAuth1): """Bitbucket OAuth authentication backend""" name = 'bitbucket' - ID_KEY = 'username' AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' - EXTRA_DATA = [ - ('username', 'username'), - ('expires', 'expires'), - ('email', 'email'), - ('first_name', 'first_name'), - ('last_name', 'last_name') - ] def get_user_details(self, response): """Return user details from Bitbucket account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('first_name', ''), - last_name=response.get('last_name', '') - ) - return {'username': response.get('username') or '', - 'email': response.get('email') or '', + fullname, first_name, last_name = self.get_user_names(response['display_name']) + + return {'username': response.get('username', ''), + 'email': response.get('email', ''), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} def user_data(self, access_token): """Return user data provided""" - # Bitbucket has a bit of an indirect route to obtain user data from an - # authenticated query: First obtain the user's email via an - # authenticated GET, then retrieve the user's primary email address or - # the top email - emails = self.get_json('https://bitbucket.org/api/1.0/emails/', + emails = self.get_json('https://api.bitbucket.org/2.0/user/emails', auth=self.oauth_auth(access_token)) + email = None - for address in reversed(emails): - if address['active']: - email = address['email'] - if address['primary']: - break + + for address in reversed(emails['values']): + email = address['email'] + if address['is_primary']: + break + + if self.setting('VERIFIED_EMAILS_ONLY', False) and not address['is_confirmed']: + raise AuthForbidden( + self, 'Bitbucket account has no verified email' + ) + + user = self.get_json('https://api.bitbucket.org/2.0/user', + auth=self.oauth_auth(access_token)) if email: - return dict(self.get_json('https://bitbucket.org/api/1.0/users/' + - email)['user'], - email=email) - elif self.setting('VERIFIED_EMAILS_ONLY', False): - raise AuthForbidden(self, - 'Bitbucket account has any verified email') - else: - return {} + user['email'] = email + + return user diff --git a/social/tests/backends/test_bitbucket.py b/social/tests/backends/test_bitbucket.py index 3e8f244f5..0c0f91202 100644 --- a/social/tests/backends/test_bitbucket.py +++ b/social/tests/backends/test_bitbucket.py @@ -9,7 +9,7 @@ class BitbucketOAuth1Test(OAuth1Test): backend_path = 'social.backends.bitbucket.BitbucketOAuth' - user_data_url = 'https://bitbucket.org/api/1.0/users/foo@bar.com' + user_data_url = 'https://api.bitbucket.org/2.0/user' expected_username = 'foobar' access_token_body = json.dumps({ 'access_token': 'foobar', @@ -20,46 +20,73 @@ class BitbucketOAuth1Test(OAuth1Test): 'oauth_token': 'foobar', 'oauth_callback_confirmed': 'true' }) - emails_body = json.dumps([{ - 'active': True, - 'email': 'foo@bar.com', - 'primary': True - }]) + emails_body = json.dumps({ + u'page': 1, + u'pagelen': 10, + u'size': 2, + u'values': [ + { + u'email': u'foo@bar.com', + u'is_confirmed': True, + u'is_primary': True, + u'links': { u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + u'type': u'email' + }, + { + u'email': u'not@confirme.com', + u'is_confirmed': False, + u'is_primary': False, + u'links': {u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/not@confirmed.com'}}, + u'type': u'email' + } + ] + }) user_data_body = json.dumps({ - 'user': { - 'username': 'foobar', - 'first_name': 'Foo', - 'last_name': 'Bar', - 'display_name': 'Foo Bar', - 'is_team': False, - 'avatar': 'https://secure.gravatar.com/avatar/' - '5280f15cedf540b544eecc30fcf3027c?' - 'd=https%3A%2F%2Fd3oaxc4q5k2d6q.cloudfront.net%2Fm%2F' - '9e262ba34f96%2Fimg%2Fdefault_avatar%2F32%2F' - 'user_blue.png&s=32', - 'resource_uri': '/1.0/users/foobar' - } + u'created_on': u'2012-03-29T18:07:38+00:00', + u'display_name': u'Foo Bar', + u'links': { + u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'}, + u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'}, + u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'}, + u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'}, + u'html': {u'href': u'https://bitbucket.org/foobar'}, + u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'}, + u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}}, + u'location': u'Fooville, Bar', + u'type': u'user', + u'username': u'foobar', + u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}', + u'website': u'http://foobar.com' }) def test_login(self): HTTPretty.register_uri(HTTPretty.GET, - 'https://bitbucket.org/api/1.0/emails/', + 'https://api.bitbucket.org/2.0/user/emails', status=200, body=self.emails_body) self.do_login() def test_partial_pipeline(self): HTTPretty.register_uri(HTTPretty.GET, - 'https://bitbucket.org/api/1.0/emails/', + 'https://api.bitbucket.org/2.0/user/emails', status=200, body=self.emails_body) self.do_partial_pipeline() class BitbucketOAuth1FailTest(BitbucketOAuth1Test): - emails_body = json.dumps([{ - 'active': False, - 'email': 'foo@bar.com', - 'primary': True - }]) + emails_body = json.dumps({ + u'page': 1, + u'pagelen': 10, + u'size': 1, + u'values': [ + { + u'email': u'foo@bar.com', + u'is_confirmed': False, + u'is_primary': True, + u'links': { u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + u'type': u'email' + } + ] + }) def test_login(self): self.strategy.set_settings({ From 0a4b744cacc0a5a5a5f586d1368f387b9b1187d8 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 21 Jun 2015 09:01:06 -0500 Subject: [PATCH 619/890] Updated Bitbucket backend to use the UUID as the ID_KEY Reference: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs#UsetheBitbucketRESTAPIs-uuid-mainUniversallyUniqueIdentifier(UUID) --- social/backends/bitbucket.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index 8bb083e7e..c0574659a 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -13,6 +13,10 @@ class BitbucketOAuth(BaseOAuth1): REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' + # Bitbucket usernames can change. The account ID should always be the UUID + # See: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs + ID_KEY = 'uuid' + def get_user_details(self, response): """Return user details from Bitbucket account""" fullname, first_name, last_name = self.get_user_names(response['display_name']) From 2853cfb2ff11cd3d149ba563ad63fc3a948df254 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 21 Jun 2015 15:06:45 -0500 Subject: [PATCH 620/890] Added an OAuth2 backend for Bitbucket --- docs/backends/bitbucket.rst | 37 +++++--- social/backends/bitbucket.py | 74 ++++++++++++--- social/backends/oauth.py | 8 ++ social/tests/backends/test_bitbucket.py | 121 ++++++++++++++++++------ 4 files changed, 184 insertions(+), 56 deletions(-) diff --git a/docs/backends/bitbucket.rst b/docs/backends/bitbucket.rst index a66478b26..00809a3c4 100644 --- a/docs/backends/bitbucket.rst +++ b/docs/backends/bitbucket.rst @@ -1,26 +1,37 @@ Bitbucket ========= -Bitbucket works similar to Twitter OAuth. +Bitbucket supports both OAuth2 and OAuth1 logins. -- Register a new application by emailing ``support@bitbucket.org`` with an - application name and a bit of a description, +1. Register a new OAuth Consumer by following the instructions in the + Bitbucket documentation: `OAuth on Bitbucket`_ + + Note: For OAuth2, your consumer MUST have the "account" scope otherwise + the user profile information (username, name, etc.) won't be accessible. + +2. Configure the appropriate settings for OAuth2 or OAuth1 (see below). + +OAuth2 +------ - Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - SOCIAL_AUTH_BITBUCKET_KEY = '' - SOCIAL_AUTH_BITBUCKET_SECRET = '' + SOCIAL_AUTH_BITBUCKET_OAUTH2_KEY = '' + SOCIAL_AUTH_BITBUCKET_OAUTH2_SECRET = '' +- If you would like to restrict access to only users with verified e-mail + addresses, set ``SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY = True`` +OAuth1 +------ -Settings --------- +- OAuth1 works similarly to OAuth2, but you must fill in the following settings + instead:: -Sometimes Bitbucket users don't have a verified email address, making it -impossible to get the basic user information to continue the auth process. -It's possible to avoid these users with this setting:: + SOCIAL_AUTH_BITBUCKET_KEY = '' + SOCIAL_AUTH_BITBUCKET_SECRET = '' - SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY = True +- If you would like to restrict access to only users with verified e-mail + addresses, set ``SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY = True`` -By default the setting is set to ``False`` since it's possible for a project to -gather this information by other methods. +.. _OAuth on Bitbucket: https://confluence.atlassian.com/display/BITBUCKET/OAuth+on+Bitbucket diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index c0574659a..fd638803d 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -1,18 +1,12 @@ """ -Bitbucket OAuth1 backend, docs at: +Bitbucket OAuth2 and OAuth1 backends, docs at: http://psa.matiasaguirre.net/docs/backends/bitbucket.html """ from social.exceptions import AuthForbidden -from social.backends.oauth import BaseOAuth1 +from social.backends.oauth import BaseOAuth1, BaseOAuth2 -class BitbucketOAuth(BaseOAuth1): - """Bitbucket OAuth authentication backend""" - name = 'bitbucket' - AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' - REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' - ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' - +class BitbucketOAuthBase(object): # Bitbucket usernames can change. The account ID should always be the UUID # See: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs ID_KEY = 'uuid' @@ -27,10 +21,9 @@ def get_user_details(self, response): 'first_name': first_name, 'last_name': last_name} - def user_data(self, access_token): + def user_data(self, access_token, *args, **kwargs): """Return user data provided""" - emails = self.get_json('https://api.bitbucket.org/2.0/user/emails', - auth=self.oauth_auth(access_token)) + emails = self._get_emails(access_token) email = None @@ -44,10 +37,63 @@ def user_data(self, access_token): self, 'Bitbucket account has no verified email' ) - user = self.get_json('https://api.bitbucket.org/2.0/user', - auth=self.oauth_auth(access_token)) + user = self._get_user(access_token) if email: user['email'] = email return user + + def _get_user(self, access_token=None): + raise NotImplementedError + + def _get_emails(self, access_token=None): + raise NotImplementedError + + +class BitbucketOAuth2(BitbucketOAuthBase, BaseOAuth2): + name = 'bitbucket-oauth2' + SCOPE_SEPARATOR = ' ' + AUTHORIZATION_URL = 'https://bitbucket.org/site/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('scopes', 'scopes'), + ('expires_in', 'expires'), + ('token_type', 'token_type'), + ('refresh_token', 'refresh_token') + ] + + def auth_complete_credentials(self): + return self.get_key_and_secret() + + def _get_user(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user', + params={'access_token': access_token}) + + def _get_emails(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user/emails', + params={'access_token': access_token}) + + def refresh_token(self, *args, **kwargs): + raise NotImplementedError('Refresh tokens for Bitbucket have not been implemented') + + +class BitbucketOAuth(BitbucketOAuthBase, BaseOAuth1): + """Bitbucket OAuth authentication backend""" + name = 'bitbucket' + AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' + REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' + ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' + + def oauth_auth(self, *args, **kwargs): + return super(BitbucketOAuth, self).oauth_auth(*args, **kwargs) + + def _get_user(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user', + auth=self.oauth_auth(access_token)) + + def _get_emails(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user/emails', + auth=self.oauth_auth(access_token)) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index d15460669..0396495c4 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -350,6 +350,9 @@ def auth_complete_params(self, state=None): 'redirect_uri': self.get_redirect_uri(state) } + def auth_complete_credentials(self): + return None + def auth_headers(self): return {'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'} @@ -371,12 +374,15 @@ def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" state = self.validate_state() self.process_error(self.data) + response = self.request_access_token( self.access_token_url(), data=self.auth_complete_params(state), headers=self.auth_headers(), + auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD ) + print(dict(response)) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) @@ -384,6 +390,8 @@ def auth_complete(self, *args, **kwargs): @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" + print(args) + print(kwargs) data = self.user_data(access_token, *args, **kwargs) response = kwargs.get('response') or {} response.update(data or {}) diff --git a/social/tests/backends/test_bitbucket.py b/social/tests/backends/test_bitbucket.py index 0c0f91202..400ea5f66 100644 --- a/social/tests/backends/test_bitbucket.py +++ b/social/tests/backends/test_bitbucket.py @@ -4,22 +4,32 @@ from social.p3 import urlencode from social.exceptions import AuthForbidden -from social.tests.backends.oauth import OAuth1Test +from social.tests.backends.oauth import OAuth1Test, OAuth2Test -class BitbucketOAuth1Test(OAuth1Test): - backend_path = 'social.backends.bitbucket.BitbucketOAuth' +class BitbucketOAuthMixin(object): user_data_url = 'https://api.bitbucket.org/2.0/user' expected_username = 'foobar' - access_token_body = json.dumps({ - 'access_token': 'foobar', - 'token_type': 'bearer' - }) - request_token_body = urlencode({ - 'oauth_token_secret': 'foobar-secret', - 'oauth_token': 'foobar', - 'oauth_callback_confirmed': 'true' + bb_api_user_emails = 'https://api.bitbucket.org/2.0/user/emails' + + user_data_body = json.dumps({ + u'created_on': u'2012-03-29T18:07:38+00:00', + u'display_name': u'Foo Bar', + u'links': { + u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'}, + u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'}, + u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'}, + u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'}, + u'html': {u'href': u'https://bitbucket.org/foobar'}, + u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'}, + u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}}, + u'location': u'Fooville, Bar', + u'type': u'user', + u'username': u'foobar', + u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}', + u'website': u'http://foobar.com' }) + emails_body = json.dumps({ u'page': 1, u'pagelen': 10, @@ -41,33 +51,31 @@ class BitbucketOAuth1Test(OAuth1Test): } ] }) - user_data_body = json.dumps({ - u'created_on': u'2012-03-29T18:07:38+00:00', - u'display_name': u'Foo Bar', - u'links': { - u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'}, - u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'}, - u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'}, - u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'}, - u'html': {u'href': u'https://bitbucket.org/foobar'}, - u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'}, - u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}}, - u'location': u'Fooville, Bar', - u'type': u'user', - u'username': u'foobar', - u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}', - u'website': u'http://foobar.com' + + +class BitbucketOAuth1Test(BitbucketOAuthMixin, OAuth1Test): + backend_path = 'social.backends.bitbucket.BitbucketOAuth' + + request_token_body = urlencode({ + 'oauth_token_secret': 'foobar-secret', + 'oauth_token': 'foobar', + 'oauth_callback_confirmed': 'true' + }) + + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' }) def test_login(self): HTTPretty.register_uri(HTTPretty.GET, - 'https://api.bitbucket.org/2.0/user/emails', + self.bb_api_user_emails, status=200, body=self.emails_body) self.do_login() def test_partial_pipeline(self): HTTPretty.register_uri(HTTPretty.GET, - 'https://api.bitbucket.org/2.0/user/emails', + self.bb_api_user_emails, status=200, body=self.emails_body) self.do_partial_pipeline() @@ -101,3 +109,58 @@ def test_partial_pipeline(self): }) with self.assertRaises(AuthForbidden): super(BitbucketOAuth1FailTest, self).test_partial_pipeline() + + +class BitbucketOAuth2Test(BitbucketOAuthMixin, OAuth2Test): + backend_path = 'social.backends.bitbucket.BitbucketOAuth2' + + access_token_body = json.dumps({ + 'access_token': 'foobar_access', + 'scopes': 'foo_scope', + 'expires_in': 3600, + 'refresh_token': 'foobar_refresh', + 'token_type': 'bearer' + }) + + def test_login(self): + HTTPretty.register_uri(HTTPretty.GET, + self.bb_api_user_emails, + status=200, body=self.emails_body) + self.do_login() + + def test_partial_pipeline(self): + HTTPretty.register_uri(HTTPretty.GET, + self.bb_api_user_emails, + status=200, body=self.emails_body) + self.do_partial_pipeline() + + +class BitbucketOAuth2FailTest(BitbucketOAuth2Test): + emails_body = json.dumps({ + u'page': 1, + u'pagelen': 10, + u'size': 1, + u'values': [ + { + u'email': u'foo@bar.com', + u'is_confirmed': False, + u'is_primary': True, + u'links': { u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + u'type': u'email' + } + ] + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY': True + }) + with self.assertRaises(AuthForbidden): + super(BitbucketOAuth2FailTest, self).test_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY': True + }) + with self.assertRaises(AuthForbidden): + super(BitbucketOAuth2FailTest, self).test_partial_pipeline() From 76a27b293f2ea3fce97e80c24fc7c1a300a24e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 24 Jun 2015 12:36:21 -0300 Subject: [PATCH 621/890] v0.2.11 --- Changelog | 43 +++++++++++++++++++++++++++++++++++++++++++ social/__init__.py | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 01e799d13..bfc5aeb40 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,27 @@ +2015-06-24 v0.2.11 +================== + + * 2015-06-24 Matías Aguirre + v0.2.11 + + * 2015-06-19 Matías Aguirre + Link docs + + * 2015-06-19 Matías Aguirre + PEP8 + + * 2015-06-18 Braden MacDonald + Added documentation + + * 2015-06-17 Matías Aguirre + PEP8 + 2015-05-29 v0.2.10 ================== + * 2015-05-29 Matías Aguirre + v0.2.10 + * 2015-05-29 Matías Aguirre Newline at end of file @@ -49,6 +70,19 @@ * 2015-05-11 sushantgawali Added provider for Microsoft Azure Active Directory OAuth2 + * 2015-05-21 Braden MacDonald + Minor cleanups + + * 2015-05-20 Braden MacDonald + Minor consistency fix + + * 2015-05-20 Braden MacDonald + Add an integration point for extra security layers like + eduPersonEntitlement + + * 2015-05-20 Braden MacDonald + Make IdP name format slightly more flexible + * 2015-05-20 Marek Jalovec Fixes "ImportError: No module named packages.urllib3.poolmanager" error (fixes #617) @@ -62,6 +96,15 @@ * 2015-05-16 Andrew Starr-Bochicchio Add a DigitalOcean backend. + * 2015-05-11 Braden MacDonald + Add python-saml requirement (temporary commit) + + * 2015-05-08 Braden MacDonald + Tests for SAML backend + + * 2015-04-30 Braden MacDonald + SAML2 backend using OneLogin's python-saml + * 2015-05-08 Matías Aguirre Ensure that all the requirements are installed diff --git a/social/__init__.py b/social/__init__.py index 135e46aad..2f4bd214f 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 10) +version = (0, 2, 11) extra = '' __version__ = '.'.join(map(str, version)) + extra From bc311bfabe2f9f4ea5eb5fdb1dcf24d646b7788f Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 24 Jun 2015 18:15:06 -0700 Subject: [PATCH 622/890] Fix wrong placement of changelog commits in 76a27b2 --- Changelog | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Changelog b/Changelog index bfc5aeb40..76fb91905 100644 --- a/Changelog +++ b/Changelog @@ -16,6 +16,28 @@ * 2015-06-17 Matías Aguirre PEP8 + * 2015-05-21 Braden MacDonald + Minor cleanups + + * 2015-05-20 Braden MacDonald + Minor consistency fix + + * 2015-05-20 Braden MacDonald + Add an integration point for extra security layers like + eduPersonEntitlement + + * 2015-05-20 Braden MacDonald + Make IdP name format slightly more flexible + + * 2015-05-11 Braden MacDonald + Add python-saml requirement (temporary commit) + + * 2015-05-08 Braden MacDonald + Tests for SAML backend + + * 2015-04-30 Braden MacDonald + SAML2 backend using OneLogin's python-saml + 2015-05-29 v0.2.10 ================== @@ -70,19 +92,6 @@ * 2015-05-11 sushantgawali Added provider for Microsoft Azure Active Directory OAuth2 - * 2015-05-21 Braden MacDonald - Minor cleanups - - * 2015-05-20 Braden MacDonald - Minor consistency fix - - * 2015-05-20 Braden MacDonald - Add an integration point for extra security layers like - eduPersonEntitlement - - * 2015-05-20 Braden MacDonald - Make IdP name format slightly more flexible - * 2015-05-20 Marek Jalovec Fixes "ImportError: No module named packages.urllib3.poolmanager" error (fixes #617) @@ -96,15 +105,6 @@ * 2015-05-16 Andrew Starr-Bochicchio Add a DigitalOcean backend. - * 2015-05-11 Braden MacDonald - Add python-saml requirement (temporary commit) - - * 2015-05-08 Braden MacDonald - Tests for SAML backend - - * 2015-04-30 Braden MacDonald - SAML2 backend using OneLogin's python-saml - * 2015-05-08 Matías Aguirre Ensure that all the requirements are installed From 992e23c4daff3023c7e0b5d83666fb02576b6f73 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 24 Jun 2015 18:29:52 -0700 Subject: [PATCH 623/890] Use official python-saml 2.1.3 release, remove setting that is no longer included --- docs/backends/saml.rst | 14 ++++++-------- social/backends/saml.py | 2 -- social/tests/requirements.txt | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/backends/saml.rst b/docs/backends/saml.rst index 72de2fd0d..e4de6eb5e 100644 --- a/docs/backends/saml.rst +++ b/docs/backends/saml.rst @@ -10,6 +10,11 @@ users can use for authentication. For example, if your users are students, you could enable Harvard and MIT as identity providers, so that students of either of those two universities can use their campus login to access your app. +Required Dependency +------------------- + +You must install python-saml_ 2.1.3 or higher in order to use this backend. + Required Configuration ---------------------- @@ -139,14 +144,6 @@ Advanced Settings your metadata for up to 10 days, but no longer. ``metadataCacheDuration`` must be specified as an ISO 8601 duration string (e.g. `P1D` for one day). -- ``SOCIAL_AUTH_SAML_SP_NAMEID_FORMATS``: This is a list of ``NameID`` formats - accepted by your app. The default is not to specify any. Example:: - - SOCIAL_AUTH_SAML_SP_NAMEID_FORMATS = [ - 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', - 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', - ] - Advanced Usage -------------- @@ -167,5 +164,6 @@ particular, there are two methods that are designed for subclasses to override: inspecting the passed attributes parameter, do nothing to allow the user to login, or raise ``social.exceptions.AuthForbidden`` to reject the user. +.. _python-saml: https://github.com/onelogin/python-saml .. _TestShib: https://www.testshib.org/ .. _metadata: https://www.testshib.org/metadata/testshib-providers.xml diff --git a/social/backends/saml.py b/social/backends/saml.py index 745451b23..198a57451 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -167,7 +167,6 @@ class SAMLAuth(BaseAuth): Optional settings: SOCIAL_AUTH_SAML_SP_EXTRA = {} SOCIAL_AUTH_SAML_SECURITY_CONFIG = {} - SOCIAL_AUTH_SAML_SP_NAMEID_FORMATS = [] """ name = "saml" @@ -202,7 +201,6 @@ def generate_saml_config(self, idp): 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' }, 'entityId': self.setting('SP_ENTITY_ID'), - 'NameIDFormats': self.setting('SP_NAMEID_FORMATS', []), 'x509cert': self.setting('SP_PUBLIC_CERT'), 'privateKey': self.setting('SP_PRIVATE_KEY'), }, diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index 12a8d0dad..6bc042d7b 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -6,4 +6,4 @@ rednose>=0.4.1 requests>=1.1.0 PyJWT>=1.0.0,<2.0.0 unittest2==0.5.1 -git+https://github.com/open-craft/python-saml.git@9602b8133056d8c3caa7c3038761147df3d4b257#egg=python-saml +python-saml==2.1.3 From 9fc8d4e3f039b76639e59652fa89299eceb67e66 Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 25 Jun 2015 09:07:29 +0200 Subject: [PATCH 624/890] Withings Backend --- README.rst | 1 + docs/backends/index.rst | 1 + docs/backends/withings.rst | 13 +++++++++++++ social/backends/withings.py | 14 ++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 docs/backends/withings.rst create mode 100644 social/backends/withings.py diff --git a/README.rst b/README.rst index ef2e99254..0eea46df2 100644 --- a/README.rst +++ b/README.rst @@ -120,6 +120,7 @@ or current ones extended): * Twitter_ OAuth1 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 + * Withings_ OAuth1 * Wunderlist_ OAuth2 * Xing_ OAuth1 * Yahoo_ OpenId and OAuth2 diff --git a/docs/backends/index.rst b/docs/backends/index.rst index fe9590a09..84569469b 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -132,6 +132,7 @@ Social backends vimeo vk weibo + withings wunderlist xing yahoo diff --git a/docs/backends/withings.rst b/docs/backends/withings.rst new file mode 100644 index 000000000..931626fc0 --- /dev/null +++ b/docs/backends/withings.rst @@ -0,0 +1,13 @@ +Withings +========= + +Withings uses OAuth v1 for Authentication. + +- Register a new application at the `Withings API`_, and + +- fill ``Client ID`` and ``Client Secret`` from withings.com values in the settings:: + + SOCIAL_AUTH_WITHINGS_KEY = '' + SOCIAL_AUTH_WITHINGS_SECRET = '' + +.. _Withings API: https://oauth.withings.com/partner/add diff --git a/social/backends/withings.py b/social/backends/withings.py new file mode 100644 index 000000000..dbe5d6d1c --- /dev/null +++ b/social/backends/withings.py @@ -0,0 +1,14 @@ +from social.backends.oauth import BaseOAuth1 + + +class WithingsOAuth(BaseOAuth1): + name = 'withings' + AUTHORIZATION_URL = 'https://oauth.withings.com/account/authorize' + REQUEST_TOKEN_URL = 'https://oauth.withings.com/account/request_token' + ACCESS_TOKEN_URL = 'https://oauth.withings.com/account/access_token' + ID_KEY = 'userid' + + def get_user_details(self, response): + """Return user details from Withings account""" + return {'userid': response['access_token']['userid'], + 'email': ''} From 237bfd08af0ce785d60b0854059149a547079ea9 Mon Sep 17 00:00:00 2001 From: Igor Serko Date: Sun, 28 Jun 2015 18:36:55 +0100 Subject: [PATCH 625/890] added support for Github Enterprise --- .gitignore | 2 + docs/backends/github_enterprise.rst | 53 ++++ social/backends/github.py | 34 ++- social/backends/github_enterprise.py | 51 ++++ .../tests/backends/test_github_enterprise.py | 243 ++++++++++++++++++ 5 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 docs/backends/github_enterprise.rst create mode 100644 social/backends/github_enterprise.py create mode 100644 social/tests/backends/test_github_enterprise.py diff --git a/.gitignore b/.gitignore index cf1280d36..e6c34a6e0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ fabfile.py changelog.sh .DS_Store +.\#* +\#*\# diff --git a/docs/backends/github_enterprise.rst b/docs/backends/github_enterprise.rst new file mode 100644 index 000000000..33b18ec26 --- /dev/null +++ b/docs/backends/github_enterprise.rst @@ -0,0 +1,53 @@ +GitHub Enterprise +================= + +GitHub Enterprise works similar to regular Github, which is in turn based on Facebook (OAuth). + +- Register a new application on your instance of `GitHub Enterprise Developers`_, + set the callback URL to ``http://example.com/complete/github/`` replacing ``example.com`` + with your domain. + +- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings:: + + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY = '' + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_GITHUB_SCOPE = [...] + + +GitHub Enterprise for Organizations +------------------------ + +When defining authentication for organizations, use the +``GithubEnterpriseOrganizationOAuth2`` backend instead. The settings are the same as +the non-organization backend, but the names must be:: + + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_* + +Be sure to define the organization name using the setting:: + + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME = '' + +This name will be used to check that the user really belongs to the given +organization and discard it if they're not part of it. + + +GitHub Enterprise for Teams +---------------- + +Similar to ``GitHub Enterprise for Organizations``, there's a GitHub for Teams backend, +use the backend ``GithubEnterpriseTeamOAuth2``. The settings are the same as +the basic backend, but the names must be:: + + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_* + +Be sure to define the ``Team ID`` using the setting:: + + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID = '' + +This ``id`` will be used to check that the user really belongs to the given +team and discard it if they're not part of it. + +.. _GitHub Enterprise Developers: https:///settings/applications/new diff --git a/social/backends/github.py b/social/backends/github.py index 9ac091c5f..85b05dc3b 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -4,15 +4,17 @@ """ from requests import HTTPError -from social.exceptions import AuthFailed +from six.moves.urllib.parse import urljoin + from social.backends.oauth import BaseOAuth2 +from social.exceptions import AuthFailed class GithubOAuth2(BaseOAuth2): """Github OAuth authentication backend""" name = 'github' - AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize' - ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' + AUTHORIZATION_URL_SUFFIX = 'login/oauth/authorize' + ACCESS_TOKEN_URL_SUFFIX = 'login/oauth/access_token' ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' EXTRA_DATA = [ @@ -21,6 +23,18 @@ class GithubOAuth2(BaseOAuth2): ('login', 'login') ] + @property + def API_URL(self): + return 'https://api.github.com/' + + @property + def AUTHORIZATION_URL(self): + return urljoin('https://github.com/', self.AUTHORIZATION_URL_SUFFIX) + + @property + def ACCESS_TOKEN_URL(self): + return urljoin('https://github.com/', self.ACCESS_TOKEN_URL_SUFFIX) + def get_user_details(self, response): """Return user details from Github account""" fullname, first_name, last_name = self.get_user_names( @@ -55,7 +69,7 @@ def user_data(self, access_token, *args, **kwargs): return data def _user_data(self, access_token, path=None): - url = 'https://api.github.com/user{0}'.format(path or '') + url = urljoin(self.API_URL, 'user{0}'.format(path or '')) return self.get_json(url, params={'access_token': access_token}) @@ -89,9 +103,9 @@ class GithubOrganizationOAuth2(GithubMemberOAuth2): no_member_string = 'User doesn\'t belong to the organization' def member_url(self, user_data): - return 'https://api.github.com/orgs/{org}/members/{username}'\ - .format(org=self.setting('NAME'), - username=user_data.get('login')) + return urljoin(self.API_URL, 'orgs/{org}/members/{username}'.format( + org=self.setting('NAME'), + username=user_data.get('login'))) class GithubTeamOAuth2(GithubMemberOAuth2): @@ -100,6 +114,6 @@ class GithubTeamOAuth2(GithubMemberOAuth2): no_member_string = 'User doesn\'t belong to the team' def member_url(self, user_data): - return 'https://api.github.com/teams/{team_id}/members/{username}'\ - .format(team_id=self.setting('ID'), - username=user_data.get('login')) + return urljoin(self.API_URL, 'teams/{team_id}/members/{username}'.format( + team_id=self.setting('ID'), + username=user_data.get('login'))) diff --git a/social/backends/github_enterprise.py b/social/backends/github_enterprise.py new file mode 100644 index 000000000..6e7344f5f --- /dev/null +++ b/social/backends/github_enterprise.py @@ -0,0 +1,51 @@ +""" +Github Enterprise OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/github_enterprise.html +""" +from six.moves.urllib.parse import urljoin + +from social.backends.github import ( + GithubOAuth2, GithubOrganizationOAuth2, GithubTeamOAuth2) + + +def append_slash(url): + """Make sure we append a slash at the end of the URL otherwise we have issues with urljoin + Example: + >>> urlparse.urljoin('http://www.example.com/api/v3', 'user/1/') + 'http://www.example.com/api/user/1/' + """ + if not url: + return url + return "%s/" % url if not url.endswith('/') else url + + +class GithubEnterpriseMixin(object): + + @property + def API_URL(self): + return append_slash(self.setting('API_URL')) + + @property + def AUTHORIZATION_URL(self): + return urljoin(append_slash(self.setting('URL')), GithubOAuth2.AUTHORIZATION_URL_SUFFIX) + + @property + def ACCESS_TOKEN_URL(self): + return urljoin(append_slash(self.setting('URL')), GithubOAuth2.ACCESS_TOKEN_URL_SUFFIX) + + +class GithubEnterpriseOAuth2(GithubEnterpriseMixin, GithubOAuth2): + """Github Enterprise OAuth authentication backend""" + name = 'github-enterprise' + + +class GithubEnterpriseOrganizationOAuth2(GithubEnterpriseMixin, GithubOrganizationOAuth2): + """Github Enterprise OAuth2 authentication backend for organizations""" + DEFAULT_SCOPE = ['read:org'] + name = 'github-enterprise-org' + + +class GithubEnterpriseTeamOAuth2(GithubEnterpriseMixin, GithubTeamOAuth2): + """Github Enterprise OAuth2 authentication backend for teams""" + DEFAULT_SCOPE = ['read:org'] + name = 'github-enterprise-team' diff --git a/social/tests/backends/test_github_enterprise.py b/social/tests/backends/test_github_enterprise.py new file mode 100644 index 000000000..61455b16a --- /dev/null +++ b/social/tests/backends/test_github_enterprise.py @@ -0,0 +1,243 @@ +import json + +from httpretty import HTTPretty + +from social.exceptions import AuthFailed + +from social.tests.backends.oauth import OAuth2Test + + +class GithubEnterpriseOAuth2Test(OAuth2Test): + backend_path = 'social.backends.github_enterprise.GithubEnterpriseOAuth2' + user_data_url = 'https://www.example.com/api/v3/user' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'login': 'foobar', + 'id': 1, + 'avatar_url': 'https://www.example.com/images/error/foobar_happy.gif', + 'gravatar_id': 'somehexcode', + 'url': 'https://www.example.com/api/v3/users/foobar', + 'name': 'monalisa foobar', + 'company': 'GitHub', + 'blog': 'https://www.example.com/blog', + 'location': 'San Francisco', + 'email': 'foo@bar.com', + 'hireable': False, + 'bio': 'There once was...', + 'public_repos': 2, + 'public_gists': 1, + 'followers': 20, + 'following': 0, + 'html_url': 'https://www.example.com/foobar', + 'created_at': '2008-01-14T04:33:35Z', + 'type': 'User', + 'total_private_repos': 100, + 'owned_private_repos': 100, + 'private_gists': 81, + 'disk_usage': 10000, + 'collaborators': 8, + 'plan': { + 'name': 'Medium', + 'space': 400, + 'collaborators': 10, + 'private_repos': 20 + } + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'https://www.example.com/api/v3'}) + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'https://www.example.com/api/v3'}) + self.do_partial_pipeline() + + +class GithubEnterpriseOAuth2NoEmailTest(GithubEnterpriseOAuth2Test): + user_data_body = json.dumps({ + 'login': 'foobar', + 'id': 1, + 'avatar_url': 'https://www.example.com/images/error/foobar_happy.gif', + 'gravatar_id': 'somehexcode', + 'url': 'https://www.example.com/api/v3/users/foobar', + 'name': 'monalisa foobar', + 'company': 'GitHub', + 'blog': 'https://www.example.com/blog', + 'location': 'San Francisco', + 'email': '', + 'hireable': False, + 'bio': 'There once was...', + 'public_repos': 2, + 'public_gists': 1, + 'followers': 20, + 'following': 0, + 'html_url': 'https://www.example.com/foobar', + 'created_at': '2008-01-14T04:33:35Z', + 'type': 'User', + 'total_private_repos': 100, + 'owned_private_repos': 100, + 'private_gists': 81, + 'disk_usage': 10000, + 'collaborators': 8, + 'plan': { + 'name': 'Medium', + 'space': 400, + 'collaborators': 10, + 'private_repos': 20 + } + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'https://www.example.com/api/v3'}) + url = 'https://www.example.com/api/v3/user/emails' + HTTPretty.register_uri(HTTPretty.GET, url, status=200, + body=json.dumps(['foo@bar.com']), + content_type='application/json') + self.do_login() + + def test_login_next_format(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'https://www.example.com/api/v3'}) + url = 'https://www.example.com/api/v3/user/emails' + HTTPretty.register_uri(HTTPretty.GET, url, status=200, + body=json.dumps([{'email': 'foo@bar.com'}]), + content_type='application/json') + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'https://www.example.com/api/v3'}) + self.do_partial_pipeline() + + +class GithubEnterpriseOrganizationOAuth2Test(GithubEnterpriseOAuth2Test): + backend_path = 'social.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2' + + def auth_handlers(self, start_url): + url = 'https://www.example.com/api/v3/orgs/foobar/members/foobar' + HTTPretty.register_uri(HTTPretty.GET, url, status=204, body='') + return super(GithubEnterpriseOrganizationOAuth2Test, self).auth_handlers( + start_url + ) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME': 'foobar'}) + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME': 'foobar'}) + self.do_partial_pipeline() + + +class GithubEnterpriseOrganizationOAuth2FailTest(GithubEnterpriseOAuth2Test): + backend_path = 'social.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2' + + def auth_handlers(self, start_url): + url = 'https://www.example.com/api/v3/orgs/foobar/members/foobar' + HTTPretty.register_uri(HTTPretty.GET, url, status=404, + body='{"message": "Not Found"}', + content_type='application/json') + return super(GithubEnterpriseOrganizationOAuth2FailTest, self).auth_handlers( + start_url + ) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME': 'foobar'}) + with self.assertRaises(AuthFailed): + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME': 'foobar'}) + with self.assertRaises(AuthFailed): + self.do_partial_pipeline() + + +class GithubEnterpriseTeamOAuth2Test(GithubEnterpriseOAuth2Test): + backend_path = 'social.backends.github_enterprise.GithubEnterpriseTeamOAuth2' + + def auth_handlers(self, start_url): + url = 'https://www.example.com/api/v3/teams/123/members/foobar' + HTTPretty.register_uri(HTTPretty.GET, url, status=204, body='') + return super(GithubEnterpriseTeamOAuth2Test, self).auth_handlers( + start_url + ) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID': '123'}) + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID': '123'}) + self.do_partial_pipeline() + + +class GithubEnterpriseTeamOAuth2FailTest(GithubEnterpriseOAuth2Test): + backend_path = 'social.backends.github_enterprise.GithubEnterpriseTeamOAuth2' + + def auth_handlers(self, start_url): + url = 'https://www.example.com/api/v3/teams/123/members/foobar' + HTTPretty.register_uri(HTTPretty.GET, url, status=404, + body='{"message": "Not Found"}', + content_type='application/json') + return super(GithubEnterpriseTeamOAuth2FailTest, self).auth_handlers( + start_url + ) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID': '123'}) + with self.assertRaises(AuthFailed): + self.do_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL': 'https://www.example.com'}) + self.strategy.set_settings({ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL': 'https://www.example.com/api/v3'}) + self.strategy.set_settings({'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID': '123'}) + with self.assertRaises(AuthFailed): + self.do_partial_pipeline() From b220af1b5219c59735bd1f35493b0a659c627738 Mon Sep 17 00:00:00 2001 From: Maarten van Schaik Date: Fri, 3 Jul 2015 16:44:36 +0200 Subject: [PATCH 626/890] Fix cookie handling for tornado --- social/strategies/tornado_strategy.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index 7e3f11252..9a3a34974 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -41,14 +41,17 @@ def html(self, content): self.request_handler.write(content) def session_get(self, name, default=None): - return self.request_handler.get_secure_cookie(name) or default + value = self.request_handler.get_secure_cookie(name) + if value: + return json.loads(value.decode()) + return default def session_set(self, name, value): - self.request_handler.set_secure_cookie(name, str(value)) + self.request_handler.set_secure_cookie(name, json.dumps(value).encode()) def session_pop(self, name): - value = self.request_handler.get_secure_cookie(name) - self.request_handler.set_secure_cookie(name, '') + value = self.session_get(name) + self.request_handler.clear_cookie(name) return value def session_setdefault(self, name, value): From 35a48beaadb683a3ae2af0ffff2ffdee301e71fd Mon Sep 17 00:00:00 2001 From: Maarten van Schaik Date: Fri, 3 Jul 2015 16:48:33 +0200 Subject: [PATCH 627/890] Fix decoding of request args --- social/strategies/tornado_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index f10a885f1..5327569b4 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -29,7 +29,7 @@ def get_setting(self, name): def request_data(self, merge=True): # Multiple valued arguments not supported yet - return dict((key, val[0]) + return dict((key, val[0].decode()) for key, val in six.iteritems(self.request.arguments)) def request_host(self): From dd9e53cbe02c0652cca35cde6d859512de4f9e44 Mon Sep 17 00:00:00 2001 From: Maksim Sokolskiy Date: Thu, 9 Jul 2015 13:01:47 +0300 Subject: [PATCH 628/890] fix(pipeline): fix user_detail pipeline issue Change `current_value` checking to allow to change empty and protected user fields --- social/pipeline/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 215791719..aa1ddcc1a 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -85,7 +85,7 @@ def user_details(strategy, details, user=None, *args, **kwargs): for name, value in details.items(): if value and hasattr(user, name): current_value = getattr(user, name, None) - if current_value is None or name not in protected: + if not current_value or name not in protected: changed |= current_value != value setattr(user, name, value) From fff27d7b848ad12e29a8fc1160308b99ca002d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 9 Jul 2015 14:55:24 -0300 Subject: [PATCH 629/890] Add comment refering to ticket. Refs #671, #672 --- social/pipeline/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index aa1ddcc1a..38784cad6 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -84,6 +84,7 @@ def user_details(strategy, details, user=None, *args, **kwargs): # on fields defined in SOCIAL_AUTH_PROTECTED_FIELDS. for name, value in details.items(): if value and hasattr(user, name): + # Check https://github.com/omab/python-social-auth/issues/671 current_value = getattr(user, name, None) if not current_value or name not in protected: changed |= current_value != value From 8dae021252e8f9005ff407190f2f6e0c4580a7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 9 Jul 2015 15:20:18 -0300 Subject: [PATCH 630/890] Backward compatibility option. Refs #652 --- docs/backends/bitbucket.rst | 10 ++++++++++ social/backends/bitbucket.py | 13 +++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/backends/bitbucket.rst b/docs/backends/bitbucket.rst index a66478b26..6572d7a25 100644 --- a/docs/backends/bitbucket.rst +++ b/docs/backends/bitbucket.rst @@ -24,3 +24,13 @@ It's possible to avoid these users with this setting:: By default the setting is set to ``False`` since it's possible for a project to gather this information by other methods. + +Bitbucket recommends the use of UUID_ as the user identifier instead +of ``username`` since they can change and impose a security risk. For +that reason ``UUID`` is used by default, but for backward +compatibility reasons, it's possible to get the old behavior again by +defining this setting:: + + SOCIAL_AUTH_BITBUCKET_USERNAME_AS_ID = True + +.. _UUID: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index c0574659a..2d785016a 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -9,17 +9,22 @@ class BitbucketOAuth(BaseOAuth1): """Bitbucket OAuth authentication backend""" name = 'bitbucket' + ID_KEY = 'uuid' AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' - # Bitbucket usernames can change. The account ID should always be the UUID - # See: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs - ID_KEY = 'uuid' + def get_user_id(self, details, response): + id_key = self.ID_KEY + if self.setting('USERNAME_AS_ID', False): + id_key = 'username' + return response.get(id_key) def get_user_details(self, response): """Return user details from Bitbucket account""" - fullname, first_name, last_name = self.get_user_names(response['display_name']) + fullname, first_name, last_name = self.get_user_names( + response['display_name'] + ) return {'username': response.get('username', ''), 'email': response.get('email', ''), From c746ecf8ba72b5b6e12df440820dc4db30ff9f90 Mon Sep 17 00:00:00 2001 From: eshellman Date: Thu, 9 Jul 2015 16:42:30 -0400 Subject: [PATCH 631/890] Update settings.rst --- docs/configuration/settings.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index a67ddb2ed..0db8bb84d 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -76,7 +76,8 @@ results and others for error situations. ``SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/new-users-redirect-url/'`` Used to redirect new registered users, will be used in place of - ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` if defined. + ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` if defined. Note that ``?next=/foo`` is appended if present, + if you want new users to go to next, you'll need to do it yourself. ``SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/new-association-redirect-url/'`` Like ``SOCIAL_AUTH_NEW_USER_REDIRECT_URL`` but for new associated accounts From 4f58c8a582906a7acae77ed5558953a55d7ce6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 9 Jul 2015 18:03:14 -0300 Subject: [PATCH 632/890] PEP8 and code simplification --- docs/backends/github.rst | 8 +++++ docs/backends/github_enterprise.rst | 8 +++-- docs/backends/index.rst | 1 + social/backends/github.py | 48 ++++++++++++++-------------- social/backends/github_enterprise.py | 43 ++++++++++--------------- social/utils.py | 11 +++++++ 6 files changed, 66 insertions(+), 53 deletions(-) diff --git a/docs/backends/github.rst b/docs/backends/github.rst index 382c7426c..d96427568 100644 --- a/docs/backends/github.rst +++ b/docs/backends/github.rst @@ -50,4 +50,12 @@ Be sure to define the ``Team ID`` using the setting:: This ``id`` will be used to check that the user really belongs to the given team and discard it if they're not part of it. + +Github for Enterprises +---------------------- + +Check the docs :ref:`github-enterprise` if planning to use Github +Enterprises. + + .. _GitHub Developers: https://github.com/settings/applications/new diff --git a/docs/backends/github_enterprise.rst b/docs/backends/github_enterprise.rst index 33b18ec26..5d59cc907 100644 --- a/docs/backends/github_enterprise.rst +++ b/docs/backends/github_enterprise.rst @@ -1,3 +1,5 @@ +.. _github-enterprise: + GitHub Enterprise ================= @@ -14,11 +16,11 @@ GitHub Enterprise works similar to regular Github, which is in turn based on Fac - Also it's possible to define extra permissions with:: - SOCIAL_AUTH_GITHUB_SCOPE = [...] + SOCIAL_AUTH_GITHUB_ENTERPRISE_SCOPE = [...] GitHub Enterprise for Organizations ------------------------- +----------------------------------- When defining authentication for organizations, use the ``GithubEnterpriseOrganizationOAuth2`` backend instead. The settings are the same as @@ -35,7 +37,7 @@ organization and discard it if they're not part of it. GitHub Enterprise for Teams ----------------- +--------------------------- Similar to ``GitHub Enterprise for Organizations``, there's a GitHub for Teams backend, use the backend ``GithubEnterpriseTeamOAuth2``. The settings are the same as diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 84569469b..ec66b1ded 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -77,6 +77,7 @@ Social backends flickr foursquare github + github_enterprise google instagram jawbone diff --git a/social/backends/github.py b/social/backends/github.py index 85b05dc3b..f5d875644 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -13,8 +13,9 @@ class GithubOAuth2(BaseOAuth2): """Github OAuth authentication backend""" name = 'github' - AUTHORIZATION_URL_SUFFIX = 'login/oauth/authorize' - ACCESS_TOKEN_URL_SUFFIX = 'login/oauth/access_token' + API_URL = 'https://api.github.com/' + AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize' + ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ',' EXTRA_DATA = [ @@ -23,17 +24,8 @@ class GithubOAuth2(BaseOAuth2): ('login', 'login') ] - @property - def API_URL(self): - return 'https://api.github.com/' - - @property - def AUTHORIZATION_URL(self): - return urljoin('https://github.com/', self.AUTHORIZATION_URL_SUFFIX) - - @property - def ACCESS_TOKEN_URL(self): - return urljoin('https://github.com/', self.ACCESS_TOKEN_URL_SUFFIX) + def api_url(self): + return self.API_URL def get_user_details(self, response): """Return user details from Github account""" @@ -57,10 +49,10 @@ def user_data(self, access_token, *args, **kwargs): if emails: email = emails[0] - primary_emails = [e for e in emails - if not isinstance(e, dict) or - e.get('primary')] - + primary_emails = [ + e for e in emails + if not isinstance(e, dict) or e.get('primary') + ] if primary_emails: email = primary_emails[0] if isinstance(email, dict): @@ -69,7 +61,7 @@ def user_data(self, access_token, *args, **kwargs): return data def _user_data(self, access_token, path=None): - url = urljoin(self.API_URL, 'user{0}'.format(path or '')) + url = urljoin(self.api_url(), 'user{0}'.format(path or '')) return self.get_json(url, params={'access_token': access_token}) @@ -103,9 +95,13 @@ class GithubOrganizationOAuth2(GithubMemberOAuth2): no_member_string = 'User doesn\'t belong to the organization' def member_url(self, user_data): - return urljoin(self.API_URL, 'orgs/{org}/members/{username}'.format( - org=self.setting('NAME'), - username=user_data.get('login'))) + return urljoin( + self.api_url(), + 'orgs/{org}/members/{username}'.format( + org=self.setting('NAME'), + username=user_data.get('login') + ) + ) class GithubTeamOAuth2(GithubMemberOAuth2): @@ -114,6 +110,10 @@ class GithubTeamOAuth2(GithubMemberOAuth2): no_member_string = 'User doesn\'t belong to the team' def member_url(self, user_data): - return urljoin(self.API_URL, 'teams/{team_id}/members/{username}'.format( - team_id=self.setting('ID'), - username=user_data.get('login'))) + return urljoin( + self.api_url(), + 'teams/{team_id}/members/{username}'.format( + team_id=self.setting('ID'), + username=user_data.get('login') + ) + ) diff --git a/social/backends/github_enterprise.py b/social/backends/github_enterprise.py index 6e7344f5f..656a92e6d 100644 --- a/social/backends/github_enterprise.py +++ b/social/backends/github_enterprise.py @@ -4,34 +4,23 @@ """ from six.moves.urllib.parse import urljoin -from social.backends.github import ( - GithubOAuth2, GithubOrganizationOAuth2, GithubTeamOAuth2) - - -def append_slash(url): - """Make sure we append a slash at the end of the URL otherwise we have issues with urljoin - Example: - >>> urlparse.urljoin('http://www.example.com/api/v3', 'user/1/') - 'http://www.example.com/api/user/1/' - """ - if not url: - return url - return "%s/" % url if not url.endswith('/') else url +from social.utils import append_slash +from social.backends.github import GithubOAuth2, GithubOrganizationOAuth2, \ + GithubTeamOAuth2 class GithubEnterpriseMixin(object): - - @property - def API_URL(self): + def api_url(self): return append_slash(self.setting('API_URL')) - @property - def AUTHORIZATION_URL(self): - return urljoin(append_slash(self.setting('URL')), GithubOAuth2.AUTHORIZATION_URL_SUFFIX) + def authorization_url(self): + return self._url('login/oauth/authorize') + + def access_token_url(self): + return self._url('login/oauth/access_token') - @property - def ACCESS_TOKEN_URL(self): - return urljoin(append_slash(self.setting('URL')), GithubOAuth2.ACCESS_TOKEN_URL_SUFFIX) + def _url(self, path): + return urljoin(append_slash(self.setting('URL')), path) class GithubEnterpriseOAuth2(GithubEnterpriseMixin, GithubOAuth2): @@ -39,13 +28,15 @@ class GithubEnterpriseOAuth2(GithubEnterpriseMixin, GithubOAuth2): name = 'github-enterprise' -class GithubEnterpriseOrganizationOAuth2(GithubEnterpriseMixin, GithubOrganizationOAuth2): - """Github Enterprise OAuth2 authentication backend for organizations""" - DEFAULT_SCOPE = ['read:org'] +class GithubEnterpriseOrganizationOAuth2(GithubEnterpriseMixin, + GithubOrganizationOAuth2): + """Github Enterprise OAuth2 authentication backend for + organizations""" name = 'github-enterprise-org' + DEFAULT_SCOPE = ['read:org'] class GithubEnterpriseTeamOAuth2(GithubEnterpriseMixin, GithubTeamOAuth2): """Github Enterprise OAuth2 authentication backend for teams""" - DEFAULT_SCOPE = ['read:org'] name = 'github-enterprise-team' + DEFAULT_SCOPE = ['read:org'] diff --git a/social/utils.py b/social/utils.py index 2cd41df43..c2db351af 100644 --- a/social/utils.py +++ b/social/utils.py @@ -235,3 +235,14 @@ def wrapper(*args, **kwargs): else: raise return wrapper + + +def append_slash(url): + """Make sure we append a slash at the end of the URL otherwise we + have issues with urljoin Example: + >>> urlparse.urljoin('http://www.example.com/api/v3', 'user/1/') + 'http://www.example.com/api/user/1/' + """ + if url and not url.endswith('/'): + url = '{0}/'.format(url) + return url From 413cd86af4fa836f979468b1cadef289197e3410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 9 Jul 2015 18:04:20 -0300 Subject: [PATCH 633/890] Fix doc title in withins file --- docs/backends/withings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/withings.rst b/docs/backends/withings.rst index 931626fc0..a9f5a6ea7 100644 --- a/docs/backends/withings.rst +++ b/docs/backends/withings.rst @@ -1,5 +1,5 @@ Withings -========= +======== Withings uses OAuth v1 for Authentication. From e17367b4e5f865db4947fc4139baa0974d4a7326 Mon Sep 17 00:00:00 2001 From: Maarten van Schaik Date: Fri, 10 Jul 2015 11:08:45 +0200 Subject: [PATCH 634/890] Fix redirect_uri issue with tornado reversed url When reversing URLs, tornado doesn't interpret the regex optional symbol '?'. This causes the redirect_uri to be https://example.com/complete/mybackend/? with the question mark appended. Some providers will simply append to this uri, causing URLs like https://example.com/complete/mybackend/??code=.... with two question marks. This makes the interpretation of the query string fail. The provider in this case is https://github.com/juanifioren/django-oidc-provider. Arguably that library should be smarter in constructing the redirection, but removing the question mark from the uri solves these kind of issues. Alternatively we could strip the question mark from the uri in the tornado strategy, but this seemed simpler. --- social/apps/tornado_app/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/tornado_app/routes.py b/social/apps/tornado_app/routes.py index 30b78395b..b671f9feb 100644 --- a/social/apps/tornado_app/routes.py +++ b/social/apps/tornado_app/routes.py @@ -5,7 +5,7 @@ SOCIAL_AUTH_ROUTES = [ url(r'/login/(?P[^/]+)/?', AuthHandler, name='begin'), - url(r'/complete/(?P[^/]+)/?', CompleteHandler, name='complete'), + url(r'/complete/(?P[^/]+)/', CompleteHandler, name='complete'), url(r'/disconnect/(?P[^/]+)/?', DisconnectHandler, name='disconnect'), url(r'/disconnect/(?P[^/]+)/(?P\d+)/?', From a7eaa315fdb56dc530fbcecbf64abce44484b5f3 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Fri, 10 Jul 2015 18:03:37 +0200 Subject: [PATCH 635/890] Fix 'QueryDict' object has no attribute 'dicts' --- social/backends/shopify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 35bca8021..e09d7771d 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -43,7 +43,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): details, *args, **kwargs) session = self.shopifyAPI.Session(self.data.get('shop').strip()) # Get, and store the permanent token - token = session.request_token(data['access_token'].dicts[1]) + token = session.request_token(data['access_token'][0]) data['access_token'] = token return dict(data) From 80336532878bd78f994dd35b3646f0722d425c9f Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Fri, 10 Jul 2015 18:08:58 +0200 Subject: [PATCH 636/890] Use key --- social/backends/shopify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/shopify.py b/social/backends/shopify.py index e09d7771d..547ad9fdf 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -43,7 +43,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): details, *args, **kwargs) session = self.shopifyAPI.Session(self.data.get('shop').strip()) # Get, and store the permanent token - token = session.request_token(data['access_token'][0]) + token = session.request_token(data['access_token']['code']) data['access_token'] = token return dict(data) From 84b89375b6b1eaa603d679295574fa74093fa29e Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Fri, 10 Jul 2015 18:11:12 +0200 Subject: [PATCH 637/890] Use full --- social/backends/shopify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 547ad9fdf..861b1a6e7 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -43,7 +43,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): details, *args, **kwargs) session = self.shopifyAPI.Session(self.data.get('shop').strip()) # Get, and store the permanent token - token = session.request_token(data['access_token']['code']) + token = session.request_token(data['access_token']) data['access_token'] = token return dict(data) From 7576316d1079e2e9c91b72bd3a868ca0e45405d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 10 Jul 2015 14:18:58 -0300 Subject: [PATCH 638/890] v0.2.12 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 2f4bd214f..73c43df11 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 11) +version = (0, 2, 12) extra = '' __version__ = '.'.join(map(str, version)) + extra From 6e2484d43c1e6ee153143f1dc1bcc8c76a0b9cfa Mon Sep 17 00:00:00 2001 From: paxapy Date: Fri, 10 Jul 2015 20:52:21 +0300 Subject: [PATCH 639/890] echosign OAuth2 backend --- social/backends/echosign.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 social/backends/echosign.py diff --git a/social/backends/echosign.py b/social/backends/echosign.py new file mode 100644 index 000000000..15c6887c3 --- /dev/null +++ b/social/backends/echosign.py @@ -0,0 +1,24 @@ +from social.backends.oauth import BaseOAuth2 + + +class EchosignOAuth2(BaseOAuth2): + name = 'echosign' + REDIRECT_STATE = False + ACCESS_TOKEN_METHOD = 'POST' + REFRESH_TOKEN_METHOD = 'POST' + REVOKE_TOKEN_METHOD = 'POST' + AUTHORIZATION_URL = 'https://secure.echosign.com/public/oauth' + ACCESS_TOKEN_URL = 'https://secure.echosign.com/oauth/token' + REFRESH_TOKEN_URL = 'https://secure.echosign.com/oauth/refresh' + REVOKE_TOKEN_URL = 'https://secure.echosign.com/oauth/revoke' + + def get_user_details(self, response): + return response + + def get_user_id(self, details, response): + return details['userInfoList'][0]['userId'] + + def user_data(self, access_token, *args, **kwargs): + return self.get_json( + 'https://api.echosign.com/api/rest/v3/users', + headers={'Access-Token': access_token}) From 204da39f8c20164ae3670bace036a279b2db0fb6 Mon Sep 17 00:00:00 2001 From: bluszcz Date: Mon, 13 Jul 2015 02:10:49 +0200 Subject: [PATCH 640/890] Added Meetup.com OAuth2 backend --- docs/backends/meetup.rst | 14 +++++++++++++ social/backends/meetup.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 docs/backends/meetup.rst create mode 100644 social/backends/meetup.py diff --git a/docs/backends/meetup.rst b/docs/backends/meetup.rst new file mode 100644 index 000000000..558c3ad3c --- /dev/null +++ b/docs/backends/meetup.rst @@ -0,0 +1,14 @@ +Meetup +====== + +Meetup.com uses OAuth2 for its auth mechanism. + +- Register a new OAuth Consumer at `Meetup Consumer Registration`_, set your + consumer name, redirect uri. + +- Fill ``key`` and ``secret`` values in the settings:: + + SOCIAL_AUTH_MEETUP_KEY = '' + SOCIAL_AUTH_MEETUP_SECRET = '' + +.. _Meetup Consumer Registration: https://secure.meetup.com/meetup_api/oauth_consumers/create diff --git a/social/backends/meetup.py b/social/backends/meetup.py new file mode 100644 index 000000000..b2e8d46e5 --- /dev/null +++ b/social/backends/meetup.py @@ -0,0 +1,41 @@ +""" +Meetup OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/soundcloud.html +""" +from social.p3 import urlencode +from social.backends.oauth import BaseOAuth2 + + +class MeetupOAuth2(BaseOAuth2): + """Meetup OAuth2 authentication backend""" + name = 'meetup' + AUTHORIZATION_URL = 'https://secure.meetup.com/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://secure.meetup.com/oauth2/access' + ACCESS_TOKEN_METHOD = 'POST' + DEFAULT_SCOPE = ['basic',] + SCOPE_SEPARATOR = ',' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('id', 'id'), + ('refresh_token', 'refresh_token'), + ('expires', 'expires') + ] + STATE_PARAMETER = 'state' + + def get_user_details(self, response): + """Return user details from Meetup account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) + + return {'username': response.get('username'), + 'email': response.get('email') or '', + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://api.meetup.com/2/member/self', + params={'access_token': access_token}) + From c49a79153759831482a9b7ba18dc4444098d7333 Mon Sep 17 00:00:00 2001 From: bluszcz Date: Mon, 13 Jul 2015 02:11:55 +0200 Subject: [PATCH 641/890] Fixed typo --- social/backends/meetup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social/backends/meetup.py b/social/backends/meetup.py index b2e8d46e5..570dc0e88 100644 --- a/social/backends/meetup.py +++ b/social/backends/meetup.py @@ -1,11 +1,10 @@ """ Meetup OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/soundcloud.html + http://psa.matiasaguirre.net/docs/backends/meetup.html """ from social.p3 import urlencode from social.backends.oauth import BaseOAuth2 - class MeetupOAuth2(BaseOAuth2): """Meetup OAuth2 authentication backend""" name = 'meetup' From 387b262d7f7eb97578b7e8e06a91d465ba81f686 Mon Sep 17 00:00:00 2001 From: bluszcz Date: Mon, 13 Jul 2015 02:15:25 +0200 Subject: [PATCH 642/890] Clean up --- social/backends/meetup.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/social/backends/meetup.py b/social/backends/meetup.py index 570dc0e88..8463a5409 100644 --- a/social/backends/meetup.py +++ b/social/backends/meetup.py @@ -14,11 +14,6 @@ class MeetupOAuth2(BaseOAuth2): DEFAULT_SCOPE = ['basic',] SCOPE_SEPARATOR = ',' REDIRECT_STATE = False - EXTRA_DATA = [ - ('id', 'id'), - ('refresh_token', 'refresh_token'), - ('expires', 'expires') - ] STATE_PARAMETER = 'state' def get_user_details(self, response): From ca8f61f9f993bd221dce853a84f534f02304ab2f Mon Sep 17 00:00:00 2001 From: Jesse Pollak Date: Tue, 14 Jul 2015 16:44:48 -0700 Subject: [PATCH 643/890] fix clef backend by access ID in correct way --- social/backends/clef.py | 14 ++++++++++++-- social/tests/backends/test_clef.py | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/social/backends/clef.py b/social/backends/clef.py index 7a034db2c..c1beff533 100644 --- a/social/backends/clef.py +++ b/social/backends/clef.py @@ -23,6 +23,9 @@ def auth_params(self, *args, **kwargs): params['redirect_url'] = params.pop('redirect_uri') return params + def get_user_id(self, response, details): + return details.get('info').get('id') + def get_user_details(self, response): """Return user details from Github account""" info = response.get('info') @@ -30,9 +33,16 @@ def get_user_details(self, response): first_name=info.get('first_name'), last_name=info.get('last_name') ) + + email = info.get('email', '') + if email: + username = email.split('@', 1)[0] + else: + username = info.get('id') + return { - 'username': response.get('clef_id'), - 'email': info.get('email', ''), + 'username': username, + 'email': email, 'fullname': fullname, 'first_name': first_name, 'last_name': last_name, diff --git a/social/tests/backends/test_clef.py b/social/tests/backends/test_clef.py index d70f9a293..2a3ae156c 100644 --- a/social/tests/backends/test_clef.py +++ b/social/tests/backends/test_clef.py @@ -6,17 +6,17 @@ class ClefOAuth2Test(OAuth2Test): backend_path = 'social.backends.clef.ClefOAuth2' user_data_url = 'https://clef.io/api/v1/info' - expected_username = '123456789' + expected_username = 'test' access_token_body = json.dumps({ 'access_token': 'foobar' }) user_data_body = json.dumps({ 'info': { + 'id': '123456789', 'first_name': 'Test', 'last_name': 'User', 'email': 'test@example.com' - }, - 'clef_id': '123456789' + } }) def test_login(self): From fe1c56c26254612cddd38caf41e60019e04cb416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 15 Jul 2015 14:14:59 -0300 Subject: [PATCH 644/890] Meetup PEP8 and link docs --- docs/backends/index.rst | 1 + social/backends/meetup.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index ec66b1ded..ad0452afe 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -91,6 +91,7 @@ Social backends loginradius mailru mapmyfitness + meetup mendeley mineid mixcloud diff --git a/social/backends/meetup.py b/social/backends/meetup.py index 8463a5409..297869714 100644 --- a/social/backends/meetup.py +++ b/social/backends/meetup.py @@ -2,16 +2,16 @@ Meetup OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/meetup.html """ -from social.p3 import urlencode from social.backends.oauth import BaseOAuth2 + class MeetupOAuth2(BaseOAuth2): """Meetup OAuth2 authentication backend""" name = 'meetup' AUTHORIZATION_URL = 'https://secure.meetup.com/oauth2/authorize' ACCESS_TOKEN_URL = 'https://secure.meetup.com/oauth2/access' ACCESS_TOKEN_METHOD = 'POST' - DEFAULT_SCOPE = ['basic',] + DEFAULT_SCOPE = ['basic'] SCOPE_SEPARATOR = ',' REDIRECT_STATE = False STATE_PARAMETER = 'state' @@ -32,4 +32,3 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://api.meetup.com/2/member/self', params={'access_token': access_token}) - From ef70edb0cbb8c38892be70e44482a49b62e9c958 Mon Sep 17 00:00:00 2001 From: Lee Jaeyoung Date: Thu, 16 Jul 2015 11:46:48 +0900 Subject: [PATCH 645/890] Add orbi backend --- docs/backends/orbi.rst | 17 +++++++++++++ social/backends/orbi.py | 39 ++++++++++++++++++++++++++++++ social/tests/backends/test_orbi.py | 30 +++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 docs/backends/orbi.rst create mode 100644 social/backends/orbi.py create mode 100644 social/tests/backends/test_orbi.py diff --git a/docs/backends/orbi.rst b/docs/backends/orbi.rst new file mode 100644 index 000000000..0cc0a7904 --- /dev/null +++ b/docs/backends/orbi.rst @@ -0,0 +1,17 @@ +Orbi +==== + +Orbi OAuth v2 for Authentication. + +- Register a new applicationat the `Orbi API`_, and + +- Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_ORBI_KEY = '' + SOCIAL_AUTH_ORBI_SECRET = '' + +- Also it's possible to define extra permissions with:: + + SOCIAL_AUTH_KAKAO_SCOPE = ['all'] + +.. _Orbi API: http://orbi.kr diff --git a/social/backends/orbi.py b/social/backends/orbi.py new file mode 100644 index 000000000..177d6fe8b --- /dev/null +++ b/social/backends/orbi.py @@ -0,0 +1,39 @@ +""" +Orbi OAuth2 backend +""" +from social.backends.oauth import BaseOAuth2 + + +class OrbiOAuth2(BaseOAuth2): + """Orbi OAuth2 authentication backend""" + name = 'orbi' + AUTHORIZATION_URL = 'https://login.orbi.kr/oauth/authorize' + ACCESS_TOKEN_URL = 'https://login.orbi.kr/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + EXTRA_DATA = [ + ('imin', 'imin'), + ('nick', 'nick'), + ('photo', 'photo'), + ('sex', 'sex'), + ('birth', 'birth'), + ] + + def get_user_id(self, details, response): + return response + + def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names(response.get('name', ''), + response.get('first_name', ''), + response.get('last_name', '')) + + return { + 'username': response.get('username', response.get('name')), + 'email': response.get('email', ''), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, + } + + def user_data(self, access_token, *args, **kwargs): + """Load user data from orbi""" + return self.get_json('https://login.orbi.kr/oauth/user/get', params={'access_token': access_token}) diff --git a/social/tests/backends/test_orbi.py b/social/tests/backends/test_orbi.py new file mode 100644 index 000000000..a35a1d56a --- /dev/null +++ b/social/tests/backends/test_orbi.py @@ -0,0 +1,30 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class OrbiOAuth2Test(OAuth2Test): + backend_path = 'social.backends.orbi.OrbiOAuth2' + user_data_url = 'https://login.orbi.kr/oauth/user/get' + access_token_body = json.dumps({ + 'access_token': 'foobar', + }) + user_data_body = json.dumps({ + 'username': 'foobar', + 'first_name': 'Foo', + 'last_name': 'Bar', + 'name': 'Foo Bar', + + 'imin': '100000', + 'nick': 'foobar', + 'photo': 'http://s3.orbi.kr/data/member/wi/wizetdev_132894975780.jpeg', + 'sex': 'M', + 'birth': '1973-08-03' + + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 5af636119211eca9071121d9a4460b832ef48808 Mon Sep 17 00:00:00 2001 From: Lee Jaeyoung Date: Thu, 16 Jul 2015 12:23:07 +0900 Subject: [PATCH 646/890] Fix test failed: get right user_id from response --- social/backends/orbi.py | 2 +- social/tests/backends/test_orbi.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/social/backends/orbi.py b/social/backends/orbi.py index 177d6fe8b..113cb480e 100644 --- a/social/backends/orbi.py +++ b/social/backends/orbi.py @@ -19,7 +19,7 @@ class OrbiOAuth2(BaseOAuth2): ] def get_user_id(self, details, response): - return response + return response.get('id') def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names(response.get('name', ''), diff --git a/social/tests/backends/test_orbi.py b/social/tests/backends/test_orbi.py index a35a1d56a..7702459ca 100644 --- a/social/tests/backends/test_orbi.py +++ b/social/tests/backends/test_orbi.py @@ -6,6 +6,7 @@ class OrbiOAuth2Test(OAuth2Test): backend_path = 'social.backends.orbi.OrbiOAuth2' user_data_url = 'https://login.orbi.kr/oauth/user/get' + expected_username = 'foobar' access_token_body = json.dumps({ 'access_token': 'foobar', }) @@ -19,8 +20,7 @@ class OrbiOAuth2Test(OAuth2Test): 'nick': 'foobar', 'photo': 'http://s3.orbi.kr/data/member/wi/wizetdev_132894975780.jpeg', 'sex': 'M', - 'birth': '1973-08-03' - + 'birth': '1973-08-03', }) def test_login(self): From 6ffcbf4e1adff9c82682c42d85e3a2876e5acdd0 Mon Sep 17 00:00:00 2001 From: Frankie Robertson Date: Thu, 16 Jul 2015 15:24:37 +0200 Subject: [PATCH 647/890] Close #622 by explicitly setting email length (compatibility with Django 1.7) --- social/apps/django_app/default/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index e30e003bb..d4432b6aa 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -89,7 +89,7 @@ class Meta: class Code(models.Model, DjangoCodeMixin): - email = models.EmailField() + email = models.EmailField(max_length=254) code = models.CharField(max_length=32, db_index=True) verified = models.BooleanField(default=False) From 3e670769d99b6c7484d07e48bf16dbd480d4e968 Mon Sep 17 00:00:00 2001 From: Jordan Reiter Date: Thu, 16 Jul 2015 14:02:20 -0400 Subject: [PATCH 648/890] text -> content solves "is not JSON serializable" Right now when you use .content, the token is bytes, not text, and so you run into an error when saving it to the session. Similar error in issue #139. --- social/backends/linkedin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index 8180a6679..739e18176 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -68,7 +68,7 @@ def unauthorized_token(self): scope = '?scope=' + self.SCOPE_SEPARATOR.join(scope) return self.request(self.REQUEST_TOKEN_URL + scope, params=self.request_token_extra_arguments(), - auth=self.oauth_auth()).content + auth=self.oauth_auth()).text class LinkedinOAuth2(BaseLinkedinAuth, BaseOAuth2): From 136e012f05bcb20f25954a7aa0893a6da5b87b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Miguel=20Neves?= Date: Sun, 19 Jul 2015 20:02:16 +0100 Subject: [PATCH 649/890] support for goclio.eu service --- social/backends/goclioeu.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 social/backends/goclioeu.py diff --git a/social/backends/goclioeu.py b/social/backends/goclioeu.py new file mode 100644 index 000000000..f5b36c46b --- /dev/null +++ b/social/backends/goclioeu.py @@ -0,0 +1,14 @@ +from social.backends.goclio import GoClioOAuth2 + + +class GoClioEuOAuth2(GoClioOAuth2): + name = 'goclioeu' + AUTHORIZATION_URL = 'https://app.goclio.eu/oauth/authorize/' + ACCESS_TOKEN_URL = 'https://app.goclio.eu/oauth/token/' + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://app.goclio.eu/api/v2/users/who_am_i', + params={'access_token': access_token} + ) From 0fb28df4fc6af244ae401c6fe661494483bb46ce Mon Sep 17 00:00:00 2001 From: Georgy Cheshkov Date: Tue, 21 Jul 2015 19:02:21 +0300 Subject: [PATCH 650/890] Remove debug printing from BaseOAuth2 backend --- social/backends/oauth.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 0396495c4..a73508a1b 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -382,7 +382,6 @@ def auth_complete(self, *args, **kwargs): auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD ) - print(dict(response)) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) @@ -390,8 +389,6 @@ def auth_complete(self, *args, **kwargs): @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" - print(args) - print(kwargs) data = self.user_data(access_token, *args, **kwargs) response = kwargs.get('response') or {} response.update(data or {}) From 109e551897b21bd79a62115e60d73c7e66b94382 Mon Sep 17 00:00:00 2001 From: Can Kaya Date: Tue, 21 Jul 2015 14:37:02 -0700 Subject: [PATCH 651/890] removed @app.teardown_request since it is called before @app.teardown_appcontext and removes current session. With @app.teardown_request session is removed just before session.commit and logged in user can not be saved on db. Added session.rollback in case of error and session remove to remove thread local session storage. --- examples/flask_example/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py index a5ebd77c8..d24fb82e4 100644 --- a/examples/flask_example/__init__.py +++ b/examples/flask_example/__init__.py @@ -55,10 +55,9 @@ def global_user(): def commit_on_success(error=None): if error is None: db_session.commit() + else: + db_session.rollback() - -@app.teardown_request -def shutdown_session(exception=None): db_session.remove() From 1c4f58246e1bf9e22542f29ed238e20491c7800b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 22 Jul 2015 12:58:28 -0300 Subject: [PATCH 652/890] Link docs and PEP8 --- docs/backends/index.rst | 1 + social/backends/orbi.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index ad0452afe..9b6e5de1d 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -100,6 +100,7 @@ Social backends nationbuilder odnoklassnikiru openstreetmap + orbi persona pixelpin pocket diff --git a/social/backends/orbi.py b/social/backends/orbi.py index 113cb480e..ab6c13ecb 100644 --- a/social/backends/orbi.py +++ b/social/backends/orbi.py @@ -22,10 +22,11 @@ def get_user_id(self, details, response): return response.get('id') def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names(response.get('name', ''), - response.get('first_name', ''), - response.get('last_name', '')) - + fullname, first_name, last_name = self.get_user_names( + response.get('name', ''), + response.get('first_name', ''), + response.get('last_name', '') + ) return { 'username': response.get('username', response.get('name')), 'email': response.get('email', ''), @@ -36,4 +37,6 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Load user data from orbi""" - return self.get_json('https://login.orbi.kr/oauth/user/get', params={'access_token': access_token}) + return self.get_json('https://login.orbi.kr/oauth/user/get', params={ + 'access_token': access_token + }) From e738e327be649bd902f0e39049398a41f2c58da8 Mon Sep 17 00:00:00 2001 From: James Little Date: Sat, 25 Jul 2015 13:13:07 +0100 Subject: [PATCH 653/890] Update main.py fix use of required decorator; doesn't work if wrapping a func already wrapped with app.route --- examples/flask_me_example/routes/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flask_me_example/routes/main.py b/examples/flask_me_example/routes/main.py index 2a2ed4e26..127986ef0 100644 --- a/examples/flask_me_example/routes/main.py +++ b/examples/flask_me_example/routes/main.py @@ -9,8 +9,8 @@ def main(): return render_template('home.html') -@login_required @app.route('/done/') +@login_required def done(): return render_template('done.html') From 5df5ea02f1710b2f3b1c4cee22646b7f8d33d8a7 Mon Sep 17 00:00:00 2001 From: Troy Grosfield Date: Wed, 29 Jul 2015 10:42:03 -0600 Subject: [PATCH 654/890] Adding a model manager for the django app. --- social/apps/django_app/default/managers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 social/apps/django_app/default/managers.py diff --git a/social/apps/django_app/default/managers.py b/social/apps/django_app/default/managers.py new file mode 100644 index 000000000..5e8769d7f --- /dev/null +++ b/social/apps/django_app/default/managers.py @@ -0,0 +1,12 @@ +from django.db import models + + +class UserSocialAuthManager(models.Manager): + """Manager for the UserSocialAuth django model.""" + + def get_social_auth(self, provider, uid): + try: + return self.select_related('user').get(provider=provider, + uid=uid) + except self.model.DoesNotExist: + return None From c4faacaf97353a864a0f7a6f514e5b64792291dd Mon Sep 17 00:00:00 2001 From: Troy Grosfield Date: Wed, 29 Jul 2015 10:43:23 -0600 Subject: [PATCH 655/890] Adding abstract UserSocialAuth model as well as a model manager class. --- social/apps/django_app/default/models.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index d4432b6aa..16cb3d130 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -12,6 +12,7 @@ DjangoCodeMixin, \ BaseDjangoStorage from social.apps.django_app.default.fields import JSONField +from social.apps.django_app.default.managers import UserSocialAuthManager USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ @@ -26,20 +27,19 @@ settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255) -class UserSocialAuth(models.Model, DjangoUserMixin): - """Social Auth association model""" +class AbstractUserSocialAuth(models.Model, DjangoUserMixin): + """Abstract Social Auth association model""" user = models.ForeignKey(USER_MODEL, related_name='social_auth') provider = models.CharField(max_length=32) uid = models.CharField(max_length=UID_LENGTH) extra_data = JSONField() + objects = UserSocialAuthManager() def __str__(self): return str(self.user) class Meta: - """Meta data""" - unique_together = ('provider', 'uid') - db_table = 'social_auth_usersocialauth' + abstract = True @classmethod def get_social_auth(cls, provider, uid): @@ -64,6 +64,15 @@ def user_model(cls): return user_model +class UserSocialAuth(AbstractUserSocialAuth): + """Social Auth association model""" + + class Meta: + """Meta data""" + unique_together = ('provider', 'uid') + db_table = 'social_auth_usersocialauth' + + class Nonce(models.Model, DjangoNonceMixin): """One use numbers""" server_url = models.CharField(max_length=NONCE_SERVER_URL_LENGTH) From e8a19546db3aff9cf27a834b338c16c94e8e260f Mon Sep 17 00:00:00 2001 From: Chris Curvey Date: Fri, 31 Jul 2015 12:32:03 -0400 Subject: [PATCH 656/890] first revision --- docs/developer_intro.rst | 164 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/developer_intro.rst diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst new file mode 100644 index 000000000..2242507ec --- /dev/null +++ b/docs/developer_intro.rst @@ -0,0 +1,164 @@ +Beginners Guide +=============== + +This is an attempt to bring together a number of concepts in python-social-auth +(psa) so that you will understand how it fits into your system. This definitely +has a Django flavor to it (because that's how I learned it). + +Understanding PSA URLs +----------------------- + +If you have not seen namespaced URLs before, you are about to be introduced. +When you add the PSA entry to your urls.py, it looks like this: + + url(r'', include('social.apps.django_app.urls', namespace='social')) + +that "namespace" part on the end is what keeps the names in the PSA-world from +colliding with the names in your app, or other 3rd-party apps. So your login +link will look like this: + + Login + +(See how "social" in the URL mapping matches the value of "namespace" in the +urls.py entry?) + +Understanding Backends +---------------------- + +PSA implements a lot of backends. Find the entry in the docs for your backend, +and if it's there, follow the steps to enable it, which come down to + + 1) Set up SOCIAL_AUTH_{backend} variables in settings.py. (The settings + vary, based on the backends) + 2) Adding your backend to AUTHENTICATION_BACKENDS in settings.py. + +If you need to implement a different backend (for instance, let's say you +want to use Intuit's OpenID), you can subclass the nearest one and override +the "name" attribute: + + from social.backends.open_id import OpenIDAuth + class IntuitOpenID(OpenIDAuth): + name = 'intuit' + +And then add your new backend to AUTHENTICATION_BACKENDS in settings.py. + + +A couple notes about the pipeline: + +The standard pipeline does not log the user in until after the pipeline has +completed. So if you get a value in the user key of the accumulative +dictionary, that implies that the user was logged in when the process started. + +Understanding the Pipeline +-------------------------- + +Reversing a URL like {% url 'social:begin' 'github' %} will give you a url like: + +http://example.com/login/github + +And clicking on that link will cfrom:(seth@hillcountryny.com)ause the "pipeline" to be started. The pipeline +is a list of functions that build up data about the user as we go through the +steps of the authentication process. (If you really want to understand the +pipeline, look at the source in social/backends/base.py, and see the +run_pipeline() function in BaseAuth.) + +The design contract for each function in the pipeline is: + +1) The pipeline starts with a four-item dictionary (the accumulative dictionary) + which is updated with the results of each function in the pipeline. The + initial four values are: + 'strategy' : contains a strategy object + 'backend' : contains the backend being used during this pipeline run + 'request' : contains a dictionary of the request keys. Note to Django + users -- this is not an HttpRequest object, it is actually + the results of request.REQUEST. + 'details' : which is an empty dict. + +2) If the function returns a dictionary or something False-ish, add the + contents of the dictionary to an accumulative dictionary (called "out" in + run_pipeline), and call the next step in the pipeline with the accumulative + dictionary. + +3) If something else is returned (for example, a subclass of HttpResponse), + then return that to the browser. + +4) If the pipeline completes, *THEN* the user is authenticated (logged in). So + if you are finding an authenticated user object while the pipeline is + running, that means that the user was logged in when the pipeline started. + +There is one pipeline for your site as a whole -- if you have backend-specific +logic, you have to make your pipeline steps smart enough to skip the step if it +is not relevant. This is as simple as: + + def my_custom_step(strategy, backend, request, details, *args, **kwargs): + if backend_name != 'my_custom_backend': + return + # otherwise, do the special steps for your custom backend + +Interrupting the Pipeline (and communicating with views) +--------------------------------------------------------- + +Let's say you want to add a custom step in the pipeline -- you want the user +to establish a password so that they can come directly to your site in the +future. We can do that with the @partial decorator, which tells the pipeline +to keep track of where it is so that it can be restarted. + +The first thing we need to do is set up a way for our views to communicate with +the pipeline. That is done by adding a value to the settings file to tell +us which values should be passed back and forth between the Django session +and the pipeline: + + FIELDS_STORED_IN_SESSION = ['local_password',] + +In our pipeline code, we would have: + + from django.shortcuts import redirect + from django.contrib.auth.models import User + from social.pipeline.partial import partial + + # partial says "we may interrupt, but we will come back here again" + @partial + def collect_password(strategy, backend, request, details, *args, **kwargs): + # request['local_password'] is set by the pipeline infrastructure + # because it exists in FIELDS_STORED_IN_SESSION + if not request.get('local_password', None): + + # if we return something besides a dict or None, then that is + # returned to the user -- in this case we will redirect to a + # view that can be used to get a password + return redirect("myapp.views.collect_password") + + # grab the user object from the database (remember that they may + # not be logged in yet) and set their password. (Assumes that the + # email address was captured in an earlier step.) + user = User.objects.get(email=kwargs['email']) + user.set_password(request['local_password']) + user.save() + + # continue the pipeline + return + +In our view code, we would have something like: + + class PasswordForm(forms.Form): + secret_word = forms.CharField(max_length=10) + + def get_user_password(request): + if request.method == 'POST': + form = PasswordForm(request.POST) + if form.is_valid(): + # because of FIELDS_STORED_IN_SESSION, this will get copied + # to the request dictionary when the pipeline is resumed + request.session['local_password'] = form.cleaned_data['secret_word'] + + # once we have the password stashed in the session, we can + # tell the pipeline to resume by using the "complete" endpoint + return redirect(reverse('social:complete', args=("backend_name,"))) + else: + form = PasswordForm() + + return render(request, "password_form.html") + +Note that the "social:complete" will re-enter the pipeline with the same +function that interrupted it (in this case, collect_password). + From 9ebfedc0682a1ad3bca4981b5b62b33e1c339775 Mon Sep 17 00:00:00 2001 From: Chris Curvey Date: Fri, 31 Jul 2015 12:42:47 -0400 Subject: [PATCH 657/890] formatting test --- docs/developer_intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst index 2242507ec..74d0e27b5 100644 --- a/docs/developer_intro.rst +++ b/docs/developer_intro.rst @@ -9,7 +9,7 @@ Understanding PSA URLs ----------------------- If you have not seen namespaced URLs before, you are about to be introduced. -When you add the PSA entry to your urls.py, it looks like this: +When you add the PSA entry to your urls.py, it looks like this:: url(r'', include('social.apps.django_app.urls', namespace='social')) From 2e8ed2a38db65348afced765851e3711d895a940 Mon Sep 17 00:00:00 2001 From: Chris Curvey Date: Fri, 31 Jul 2015 12:50:33 -0400 Subject: [PATCH 658/890] more formatting --- docs/developer_intro.rst | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst index 74d0e27b5..dff27c8d1 100644 --- a/docs/developer_intro.rst +++ b/docs/developer_intro.rst @@ -15,7 +15,7 @@ When you add the PSA entry to your urls.py, it looks like this:: that "namespace" part on the end is what keeps the names in the PSA-world from colliding with the names in your app, or other 3rd-party apps. So your login -link will look like this: +link will look like this:: Login @@ -28,13 +28,13 @@ Understanding Backends PSA implements a lot of backends. Find the entry in the docs for your backend, and if it's there, follow the steps to enable it, which come down to - 1) Set up SOCIAL_AUTH_{backend} variables in settings.py. (The settings - vary, based on the backends) - 2) Adding your backend to AUTHENTICATION_BACKENDS in settings.py. +1) Set up SOCIAL_AUTH_{backend} variables in settings.py. (The settings vary, based on the backends) + +2) Adding your backend to AUTHENTICATION_BACKENDS in settings.py. If you need to implement a different backend (for instance, let's say you want to use Intuit's OpenID), you can subclass the nearest one and override -the "name" attribute: +the "name" attribute:: from social.backends.open_id import OpenIDAuth class IntuitOpenID(OpenIDAuth): @@ -42,7 +42,6 @@ the "name" attribute: And then add your new backend to AUTHENTICATION_BACKENDS in settings.py. - A couple notes about the pipeline: The standard pipeline does not log the user in until after the pipeline has @@ -64,9 +63,7 @@ run_pipeline() function in BaseAuth.) The design contract for each function in the pipeline is: -1) The pipeline starts with a four-item dictionary (the accumulative dictionary) - which is updated with the results of each function in the pipeline. The - initial four values are: +1) The pipeline starts with a four-item dictionary (the accumulative dictionary) which is updated with the results of each function in the pipeline. The initial four values are: 'strategy' : contains a strategy object 'backend' : contains the backend being used during this pipeline run 'request' : contains a dictionary of the request keys. Note to Django @@ -74,21 +71,15 @@ The design contract for each function in the pipeline is: the results of request.REQUEST. 'details' : which is an empty dict. -2) If the function returns a dictionary or something False-ish, add the - contents of the dictionary to an accumulative dictionary (called "out" in - run_pipeline), and call the next step in the pipeline with the accumulative - dictionary. +2) If the function returns a dictionary or something False-ish, add the contents of the dictionary to an accumulative dictionary (called "out" in run_pipeline), and call the next step in the pipeline with the accumulative dictionary. -3) If something else is returned (for example, a subclass of HttpResponse), - then return that to the browser. +3) If something else is returned (for example, a subclass of HttpResponse), then return that to the browser. -4) If the pipeline completes, *THEN* the user is authenticated (logged in). So - if you are finding an authenticated user object while the pipeline is - running, that means that the user was logged in when the pipeline started. +4) If the pipeline completes, *THEN* the user is authenticated (logged in). So if you are finding an authenticated user object while the pipeline is running, that means that the user was logged in when the pipeline started. There is one pipeline for your site as a whole -- if you have backend-specific logic, you have to make your pipeline steps smart enough to skip the step if it -is not relevant. This is as simple as: +is not relevant. This is as simple as:: def my_custom_step(strategy, backend, request, details, *args, **kwargs): if backend_name != 'my_custom_backend': @@ -106,11 +97,11 @@ to keep track of where it is so that it can be restarted. The first thing we need to do is set up a way for our views to communicate with the pipeline. That is done by adding a value to the settings file to tell us which values should be passed back and forth between the Django session -and the pipeline: +and the pipeline:: FIELDS_STORED_IN_SESSION = ['local_password',] -In our pipeline code, we would have: +In our pipeline code, we would have:: from django.shortcuts import redirect from django.contrib.auth.models import User @@ -138,7 +129,7 @@ In our pipeline code, we would have: # continue the pipeline return -In our view code, we would have something like: +In our view code, we would have something like:: class PasswordForm(forms.Form): secret_word = forms.CharField(max_length=10) From 73924842e2844771340207a886795e24ee6d4619 Mon Sep 17 00:00:00 2001 From: Chris Curvey Date: Fri, 31 Jul 2015 12:51:43 -0400 Subject: [PATCH 659/890] formatting cleanups --- docs/developer_intro.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst index dff27c8d1..2e40cc7d1 100644 --- a/docs/developer_intro.rst +++ b/docs/developer_intro.rst @@ -66,9 +66,7 @@ The design contract for each function in the pipeline is: 1) The pipeline starts with a four-item dictionary (the accumulative dictionary) which is updated with the results of each function in the pipeline. The initial four values are: 'strategy' : contains a strategy object 'backend' : contains the backend being used during this pipeline run - 'request' : contains a dictionary of the request keys. Note to Django - users -- this is not an HttpRequest object, it is actually - the results of request.REQUEST. + 'request' : contains a dictionary of the request keys. Note to Django users -- this is not an HttpRequest object, it is actually the results of request.REQUEST. 'details' : which is an empty dict. 2) If the function returns a dictionary or something False-ish, add the contents of the dictionary to an accumulative dictionary (called "out" in run_pipeline), and call the next step in the pipeline with the accumulative dictionary. From e4072e0b814c20ed55a32e4bcca7128ac0a6bf3a Mon Sep 17 00:00:00 2001 From: khamaileon Date: Wed, 5 Aug 2015 13:44:49 +0200 Subject: [PATCH 660/890] Fix #703 --- social/backends/spotify.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/social/backends/spotify.py b/social/backends/spotify.py index 3074a4501..e8ab88ed7 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -21,10 +21,10 @@ class SpotifyOAuth2(BaseOAuth2): ] def auth_headers(self): + auth_str = '{0}:{1}'.format(*self.get_key_and_secret()) + b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode() return { - 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( - ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) - )) + 'Authorization': 'Basic {0}'.format(b64_auth_str) } def get_user_details(self, response): From 1a1a78cc8a65346c9df459972dde4bcc68b80bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 7 Aug 2015 18:17:32 -0300 Subject: [PATCH 661/890] Retrieve remember value from session --- social/apps/flask_app/routes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index 78c51fe1a..0d0578cc1 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -36,6 +36,9 @@ def disconnect(backend, association_id=None): def do_login(backend, user, social_user): - return login_user(user, remember=request.cookies.get('remember') or - request.args.get('remember') or - request.form.get('remember') or False) + remember = backend.strategy.session_get('remember') or \ + request.cookies.get('remember') or \ + request.args.get('remember') or \ + request.form.get('remember') or \ + False + return login_user(user, remember=remember) From 4e6e0952bafe918e5d6471a87717482750c46d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 7 Aug 2015 20:03:05 -0300 Subject: [PATCH 662/890] Fix flask remember-me functionality --- docs/configuration/flask.rst | 26 ++++++++++++++++++++++++++ social/apps/flask_app/routes.py | 9 +++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/configuration/flask.rst b/docs/configuration/flask.rst index bc364cf19..fc7ada33d 100644 --- a/docs/configuration/flask.rst +++ b/docs/configuration/flask.rst @@ -101,6 +101,32 @@ handlers to these:: return {'user': None} +Remembering sessions +-------------------- + +The users session can be remembered when specified on login. The common +implementation for this feature is to pass a parameter from the login form +(``remember_me``, ``keep``, etc), to flag the action. Flask-Login_ will mark +the session as persistent if told so. + +python-social-auth_ will check for a given name (``keep``) by default, but +since providers won't pass parameters back to the application, the value must +be persisted in the session before the authentication process happens. + +So, the following setting is required for this to work:: + + SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['keep'] + +It's possible to override the default name with this setting:: + + SOCIAL_AUTH_REMEMBER_SESSION_NAME = 'remember_me' + +Don't use the value ``remember`` since that will clash with Flask-Login_ which +pops the value from the session. + +Then just pass the parameter ``keep=1`` as a GET or POST parameter. + + Exceptions handling ------------------- diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index 0d0578cc1..6c1eec79d 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -36,9 +36,10 @@ def disconnect(backend, association_id=None): def do_login(backend, user, social_user): - remember = backend.strategy.session_get('remember') or \ - request.cookies.get('remember') or \ - request.args.get('remember') or \ - request.form.get('remember') or \ + name = backend.strategy.setting('REMEMBER_SESSION_NAME', 'keep') + remember = backend.strategy.session_get(name) or \ + request.cookies.get(name) or \ + request.args.get(name) or \ + request.form.get(name) or \ False return login_user(user, remember=remember) From 93e0cd8b929cc8e830dd7110a204aeaefba7a3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henoc=20D=C3=ADaz?= Date: Mon, 10 Aug 2015 00:42:59 -0500 Subject: [PATCH 663/890] Add support for Uber OAuth2 - Uber API v1 - Backend UberOAuth2 added - Tests for UberOAuth2 backend added - Docs for backend added --- docs/backends/uber.rst | 28 +++++++++++++++++++++ social/backends/uber.py | 39 ++++++++++++++++++++++++++++++ social/tests/backends/test_uber.py | 36 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 docs/backends/uber.rst create mode 100644 social/backends/uber.py create mode 100644 social/tests/backends/test_uber.py diff --git a/docs/backends/uber.rst b/docs/backends/uber.rst new file mode 100644 index 000000000..7aca7b97e --- /dev/null +++ b/docs/backends/uber.rst @@ -0,0 +1,28 @@ +Uber +========= + +Uber uses OAuth v2 for Authentication. + +- Register a new application at the `Uber API`_, and follow the instructions below + +OAuth2 +========= + +1. Add the Uber OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.uber.UberOAuth2', + ... + ) + +2. Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_UBER_KEY = '' + SOCIAL_AUTH_UBER_SECRET = '' + +3. Scope should be defined by using:: + + SOCIAL_AUTH_UBER_SCOPE = ['profile', 'request'] + +.. _Uber API: https://developer.uber.com/dashboard diff --git a/social/backends/uber.py b/social/backends/uber.py new file mode 100644 index 000000000..ef6c3815a --- /dev/null +++ b/social/backends/uber.py @@ -0,0 +1,39 @@ +""" +Uber OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/uber.html +""" +from social.backends.oauth import BaseOAuth2 + + +class UberOAuth2(BaseOAuth2): + name = 'uber' + ID_KEY='uuid' + SCOPE_SEPARATOR = ' ' + AUTHORIZATION_URL = 'https://login.uber.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://login.uber.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + + def auth_complete_credentials(self): + return self.get_key_and_secret() + + def get_user_details(self, response): + """Return user details from Uber account""" + email = response.get('email', '') + fullname, first_name, last_name = self.get_user_names() + return {'username': email, + 'email': email, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + client_id, client_secret = self.get_key_and_secret() + response = kwargs.pop('response') + + return self.get_json('https://api.uber.com/v1/me', headers={ + 'Authorization': '{0} {1}'.format( + response.get('token_type'), access_token + ) + } + ) diff --git a/social/tests/backends/test_uber.py b/social/tests/backends/test_uber.py new file mode 100644 index 000000000..8be37308f --- /dev/null +++ b/social/tests/backends/test_uber.py @@ -0,0 +1,36 @@ +import json + +from httpretty import HTTPretty + +from social.p3 import urlencode +from social.exceptions import AuthForbidden +from social.tests.backends.oauth import OAuth1Test, OAuth2Test + + +class UberOAuth2Test(OAuth2Test): + user_data_url = 'https://api.uber.com/v1/me' + backend_path = 'social.backends.uber.UberOAuth2' + expected_username = 'foo@bar.com' + + user_data_body = json.dumps({ + "first_name": "Foo", + "last_name": "Bar", + "email": "foo@bar.com", + "picture": "https://", + "promo_code": "barfoo", + "uuid": "91d81273-45c2-4b57-8124-d0165f8240c0" + }) + + access_token_body = json.dumps({ + "access_token": "EE1IDxytP04tJ767GbjH7ED9PpGmYvL", + "token_type": "Bearer", + "expires_in": 2592000, + "refresh_token": "Zx8fJ8qdSRRseIVlsGgtgQ4wnZBehr", + "scope": "profile history request" + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 9668c6eeac3f3eff4b3d8122d39def51f779f3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henoc=20D=C3=ADaz?= Date: Mon, 10 Aug 2015 00:50:20 -0500 Subject: [PATCH 664/890] References to UberOAuth2 backend docs added in intro.rst and backends/index.rst --- README.rst | 2 ++ docs/backends/index.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 0eea46df2..03df9cd74 100644 --- a/README.rst +++ b/README.rst @@ -118,6 +118,7 @@ or current ones extended): * Tumblr_ OAuth1 * Twilio_ Auth * Twitter_ OAuth1 + * Uber_ OAuth2 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 * Withings_ OAuth1 @@ -279,6 +280,7 @@ check `django-social-auth LICENSE`_ for details: .. _Tripit: https://www.tripit.com .. _Twilio: https://www.twilio.com .. _Twitter: http://twitter.com +.. _Uber: http://uber.com .. _VK.com: http://vk.com .. _Weibo: https://weibo.com .. _Wunderlist: https://wunderlist.com diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 9b6e5de1d..799ca6238 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -131,6 +131,7 @@ Social backends twilio twitch twitter + uber vend vimeo vk From 55dc54a481bf62618704351818d52356c4ea4da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 14 Aug 2015 03:26:04 -0300 Subject: [PATCH 665/890] Small cosmetic changes and link from main index. Refs #700 --- docs/developer_intro.rst | 47 +++++++++++++++++++++++++--------------- docs/index.rst | 1 + 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst index 2e40cc7d1..7b2cf2cf4 100644 --- a/docs/developer_intro.rst +++ b/docs/developer_intro.rst @@ -2,7 +2,7 @@ Beginners Guide =============== This is an attempt to bring together a number of concepts in python-social-auth -(psa) so that you will understand how it fits into your system. This definitely +(psa) so that you will understand how it fits into your system. This definitely has a Django flavor to it (because that's how I learned it). Understanding PSA URLs @@ -28,7 +28,8 @@ Understanding Backends PSA implements a lot of backends. Find the entry in the docs for your backend, and if it's there, follow the steps to enable it, which come down to -1) Set up SOCIAL_AUTH_{backend} variables in settings.py. (The settings vary, based on the backends) +1) Set up SOCIAL_AUTH_{backend} variables in settings.py. (The + settings vary, based on the backends) 2) Adding your backend to AUTHENTICATION_BACKENDS in settings.py. @@ -51,29 +52,40 @@ dictionary, that implies that the user was logged in when the process started. Understanding the Pipeline -------------------------- -Reversing a URL like {% url 'social:begin' 'github' %} will give you a url like: +Reversing a URL like ``{% url 'social:begin' 'github' %}`` will give you a url +like:: -http://example.com/login/github + http://example.com/login/github -And clicking on that link will cfrom:(seth@hillcountryny.com)ause the "pipeline" to be started. The pipeline +And clicking on that link will cause the "pipeline" to be started. The pipeline is a list of functions that build up data about the user as we go through the steps of the authentication process. (If you really want to understand the -pipeline, look at the source in social/backends/base.py, and see the -run_pipeline() function in BaseAuth.) +pipeline, look at the source in ``social/backends/base.py``, and see the +``run_pipeline()`` function in ``BaseAuth``.) The design contract for each function in the pipeline is: -1) The pipeline starts with a four-item dictionary (the accumulative dictionary) which is updated with the results of each function in the pipeline. The initial four values are: - 'strategy' : contains a strategy object - 'backend' : contains the backend being used during this pipeline run - 'request' : contains a dictionary of the request keys. Note to Django users -- this is not an HttpRequest object, it is actually the results of request.REQUEST. - 'details' : which is an empty dict. +1) The pipeline starts with a four-item dictionary (the accumulative dictionary) + which is updated with the results of each function in the pipeline. The + initial four values are: + 'strategy' : contains a strategy object + 'backend' : contains the backend being used during this pipeline run + 'request' : contains a dictionary of the request keys. Note to Django + users -- this is not an HttpRequest object, it is actually + the results of ``request.REQUEST``. + 'details' : which is an empty dict. -2) If the function returns a dictionary or something False-ish, add the contents of the dictionary to an accumulative dictionary (called "out" in run_pipeline), and call the next step in the pipeline with the accumulative dictionary. +2) If the function returns a dictionary or something False-ish, add the contents + of the dictionary to an accumulative dictionary (called ``out`` in + ``run_pipeline``), and call the next step in the pipeline with the + accumulative dictionary. -3) If something else is returned (for example, a subclass of HttpResponse), then return that to the browser. +3) If something else is returned (for example, a subclass of ``HttpResponse``), + then return that to the browser. -4) If the pipeline completes, *THEN* the user is authenticated (logged in). So if you are finding an authenticated user object while the pipeline is running, that means that the user was logged in when the pipeline started. +4) If the pipeline completes, *THEN* the user is authenticated (logged in). So + if you are finding an authenticated user object while the pipeline is + running, that means that the user was logged in when the pipeline started. There is one pipeline for your site as a whole -- if you have backend-specific logic, you have to make your pipeline steps smart enough to skip the step if it @@ -97,7 +109,7 @@ the pipeline. That is done by adding a value to the settings file to tell us which values should be passed back and forth between the Django session and the pipeline:: - FIELDS_STORED_IN_SESSION = ['local_password',] + SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['local_password',] In our pipeline code, we would have:: @@ -148,6 +160,5 @@ In our view code, we would have something like:: return render(request, "password_form.html") -Note that the "social:complete" will re-enter the pipeline with the same +Note that the ``social:complete`` will re-enter the pipeline with the same function that interrupted it (in this case, collect_password). - diff --git a/docs/index.rst b/docs/index.rst index 1f91df172..ca1506921 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,6 +26,7 @@ Contents: storage exceptions backends/index + developer_intro logging_out tests use_cases From 08ed0f8b5cf1e19cc49f7c214b2ba1230d2705c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 14 Aug 2015 03:30:29 -0300 Subject: [PATCH 666/890] Small cosmetic changes and link from main index. Refs #700 --- docs/developer_intro.rst | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst index 7b2cf2cf4..ebd8526cc 100644 --- a/docs/developer_intro.rst +++ b/docs/developer_intro.rst @@ -38,6 +38,7 @@ want to use Intuit's OpenID), you can subclass the nearest one and override the "name" attribute:: from social.backends.open_id import OpenIDAuth + class IntuitOpenID(OpenIDAuth): name = 'intuit' @@ -68,12 +69,17 @@ The design contract for each function in the pipeline is: 1) The pipeline starts with a four-item dictionary (the accumulative dictionary) which is updated with the results of each function in the pipeline. The initial four values are: - 'strategy' : contains a strategy object - 'backend' : contains the backend being used during this pipeline run - 'request' : contains a dictionary of the request keys. Note to Django - users -- this is not an HttpRequest object, it is actually - the results of ``request.REQUEST``. - 'details' : which is an empty dict. + + ``strategy`` + contains a strategy object + ``backend`` + contains the backend being used during this pipeline run + ``request`` + contains a dictionary of the request keys. Note to Django users -- this is + not an HttpRequest object, it is actually the results of + ``request.REQUEST``. + ``details`` + which is an empty dict. 2) If the function returns a dictionary or something False-ish, add the contents of the dictionary to an accumulative dictionary (called ``out`` in From 82aa693e7db36b0a58f890777f2d36f88650676f Mon Sep 17 00:00:00 2001 From: Ajoy Oommen Date: Fri, 14 Aug 2015 16:06:28 +0530 Subject: [PATCH 667/890] Fix typo in use_cases.rst --- docs/use_cases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 8216876f4..d020d893d 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -139,7 +139,7 @@ implemented easily):: else: return 'ERROR' -The snipped above is quite simple, it doesn't return JSON and usually this call +The snippet above is quite simple, it doesn't return JSON and usually this call will be done by AJAX. It doesn't return the user information, but that's something that can be extended and filled to suit the project where it's going to be used. From 4bca07fd4cd050b94a3dc62a6a5f0ec2093553d6 Mon Sep 17 00:00:00 2001 From: Ajoy Oommen Date: Fri, 14 Aug 2015 16:13:44 +0530 Subject: [PATCH 668/890] Fix another mistake in use_cases.rst --- docs/use_cases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index d020d893d..38c25ba09 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -7,7 +7,7 @@ Some miscellaneous options and use cases for python-social-auth_. Return the user to the original page ------------------------------------ -There's a common scenario were it's desired to return the user back to the +There's a common scenario where it's desired to return the user back to the original page from where it was requested to login. For that purpose, the usual ``next`` query-string argument is used, the value of this parameter will be stored in the session and later used to redirect the user when login was From 0a2b9f79972868a8250591659db1f30b7761dec5 Mon Sep 17 00:00:00 2001 From: Chun-Jung Lee Date: Sat, 15 Aug 2015 06:16:30 +0800 Subject: [PATCH 669/890] Support Pyramid Authentication Policies --- social/strategies/pyramid_strategy.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/social/strategies/pyramid_strategy.py b/social/strategies/pyramid_strategy.py index 57d8c383f..6ac41573c 100644 --- a/social/strategies/pyramid_strategy.py +++ b/social/strategies/pyramid_strategy.py @@ -25,7 +25,12 @@ def __init__(self, storage, request, tpl=None): def redirect(self, url): """Return a response redirect to the given URL""" - return HTTPFound(location=url) + response = getattr(self.request, 'response', None) + if response is None: + response = HTTPFound(location=url) + else: + response = HTTPFound(location=url, headers=response.headers) + return response def get_setting(self, name): """Return value for given setting name""" @@ -33,7 +38,13 @@ def get_setting(self, name): def html(self, content): """Return HTTP response with given content""" - return Response(body=content) + response = getattr(self.request, 'response', None) + if response is None: + response = Response(body=content) + else: + response = self.request.response + response.body = content + return response def request_data(self, merge=True): """Return current request data (POST or GET)""" From 51fc4b7a43846420a7aac68837ae3019de767ca4 Mon Sep 17 00:00:00 2001 From: Jerzy Spendel Date: Tue, 18 Aug 2015 15:15:12 +0200 Subject: [PATCH 670/890] Tuple in pipeline's documentation should be ended with coma --- docs/pipeline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 64e65fb60..3606ae5b8 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -71,7 +71,7 @@ The default pipeline is composed by:: 'social.pipeline.social_auth.load_extra_data', # Update the user record with any changed info from the auth service. - 'social.pipeline.user.user_details' + 'social.pipeline.user.user_details', ) From 69fa698aee3c66daf3928447d5869711fadd5a61 Mon Sep 17 00:00:00 2001 From: Jerzy Spendel Date: Tue, 18 Aug 2015 15:19:27 +0200 Subject: [PATCH 671/890] Coma at the end of every tuple and dict --- docs/pipeline.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 3606ae5b8..458eeacc4 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -86,7 +86,7 @@ ones would look like this:: 'social.pipeline.social_auth.social_user', 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details' + 'social.pipeline.user.user_details', ) Note that this assumes the user is already authenticated, and thus the ``user`` key @@ -144,7 +144,7 @@ In order to override the disconnection pipeline, just define the setting:: 'social.pipeline.disconnect.revoke_tokens', # Removes the social associations. - 'social.pipeline.disconnect.disconnect' + 'social.pipeline.disconnect.disconnect', ) @@ -303,7 +303,7 @@ returned by the provider (``Facebook`` in this example). The usual Facebook 'updated_time': '2014-01-14T15:58:35+0000', 'link': 'https://www.facebook.com/foobar', 'timezone': -3, - 'id': '100000126636010' + 'id': '100000126636010', } Let's say we are interested in storing the user profile link, the gender and @@ -333,7 +333,7 @@ the pipeline, since it needs the user instance, it needs to be put after 'path.to.save_profile', # <--- set the path to the function 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details' + 'social.pipeline.user.user_details', ) If the return value of the function is a ``dict``, the values will be merged From bdd774871a653254ef08f46ea4d0914095ad675a Mon Sep 17 00:00:00 2001 From: Jerzy Spendel Date: Wed, 19 Aug 2015 10:04:59 +0200 Subject: [PATCH 672/890] One more coma in use_cases.rst --- docs/use_cases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 38c25ba09..bebb60a81 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -90,7 +90,7 @@ function, like this:: 'social.pipeline.user.create_user', 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details' + 'social.pipeline.user.user_details', ) This feature is disabled by default because it's not 100% secure to automate From 6b18c5c55391a18d5b9e2753c26d0667d2e72324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zagdan?= Date: Thu, 20 Aug 2015 13:14:29 +0200 Subject: [PATCH 673/890] Update facebook.rst Updated FB docs to reflect changes in getting fields form public profile or email --- docs/backends/facebook.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/backends/facebook.rst b/docs/backends/facebook.rst index 67c3ea1d9..cf9136516 100644 --- a/docs/backends/facebook.rst +++ b/docs/backends/facebook.rst @@ -24,9 +24,13 @@ development resources`_: SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] - Define ``SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS`` to pass extra parameters - to https://graph.facebook.com/me when gathering the user profile data, like:: + to https://graph.facebook.com/me when gathering the user profile data (you need + to explicitly ask for fields like ``email`` using ``fields`` key):: - SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {'locale': 'ru_RU'} + SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { + 'locale': 'ru_RU', + 'fields': 'id, name, email, age_range' + } If you define a redirect URL in Facebook setup page, be sure to not define http://127.0.0.1:8000 or http://localhost:8000 because it won't work when From af518ab075dcb0f8b443ebbc2ee8d5af52ff0f5a Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 24 Aug 2015 15:27:11 +0100 Subject: [PATCH 674/890] Fix typo in pipeline doc --- docs/pipeline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 64e65fb60..496582d1b 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -262,7 +262,7 @@ responses. To enumerate a few: The server user-details response, it depends on the protocol in use (and sometimes the provider implementation of such protocol), but usually it's just a ``dict`` with the user profile details in such provider. Lots of - information related to the user is provider here, sometimes the ``scope`` + information related to the user is provided here, sometimes the ``scope`` will increase the amount of information in this response on OAuth providers. From a4918af20e09642b8ee47724a023899ef93fd22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 31 Aug 2015 12:28:21 -0300 Subject: [PATCH 675/890] Fix Google+ auth complete, update examples with updated SDK usage. Refs #316 --- docs/backends/google.rst | 118 +++++++++++------- .../example/templates/home.html | 90 ++++++++----- social/backends/google.py | 38 +++--- 3 files changed, 151 insertions(+), 95 deletions(-) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index affe7f722..64dd66edb 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -63,8 +63,9 @@ Google+ Sign-In done by their Javascript which thens calls a defined handler to complete the auth process. -* To enable the backend create an application using the `Google console`_ and - following the steps from the `official guide`_. Make sure to enable the Google+ API in the console. +* To enable the backend create an application using the `Google + console`_ and following the steps from the `official guide`_. Make + sure to enable the Google+ API in the console. * Fill in the key settings looking inside the Google console the subsection ``Credentials`` inside ``API & auth``:: @@ -81,57 +82,83 @@ auth process. ``SOCIAL_AUTH_GOOGLE_PLUS_SECRET`` corresponds to the variable ``CLIENT SECRET``. -* Create a new Django view and in its template add the Google+ Sign-In button:: - -
          - - - -
          - -
          - {% csrf_token %} - - -
          - - ``plus_id`` is the value from ``SOCIAL_AUTH_GOOGLE_PLUS_KEY``. - ``signInCallback`` is the name of your Javascript callback function. - If you would like to get user's email address and have it stored, then set - this value in `data-scope`:: - - data-scope="https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email" +* Add the sign-in button to your template, you can use the SDK button + or add your own and attacht he click handler to it (check `Google+ Identity Sign-In`_ + documentation about it):: + +
          Google+ Sign In
          * Add the Javascript snippet in the same template as above:: - + + + {% endif %} + +* Logging out -* And define your Javascript callback function:: + Logging-out can be tricky when using the the platform SDK because it + can trigger an automatic sign-in when listening to the user status + change. With the method show above, that won't happen, but if the UI + depends more in the SDK values than the backend, then things can get + out of sync easilly. To prevent this, the user should be logged-out + from Google+ platform too. This can be accomplished by doing:: @@ -223,3 +250,4 @@ supporting them you can default to the old values by defining this setting:: .. _official guide: https://developers.google.com/+/web/signin/#step_1_create_a_client_id_and_client_secret .. _Sept 1, 2014: https://developers.google.com/+/api/auth-migration#timetable .. _e3525187: https://github.com/omab/python-social-auth/commit/e35251878a88954cecf8e575eca27c63164b9f67 +.. _Google+ Identity Sign-In: https://developers.google.com/identity/sign-in/web/sign-in diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html index 3a51c5992..58b7ec8fb 100644 --- a/examples/django_example/example/templates/home.html +++ b/examples/django_example/example/templates/home.html @@ -11,7 +11,7 @@ .col-md-2 { width: 18.6667%; } .buttons { display: block; table-layout: fixed; border-radius: 7px; border: 1px solid #ccc; margin: 20px; background: #eee; padding: 30px; } - .buttons > div a { margin: 5px 10px; } + .buttons > div .btn { margin: 5px 10px; } .buttons > div:not(:first-child) { margin-top: 10px; border-top: 1px solid #ccc; padding-top: 10px; text-align: center; } .user-details { text-align: center; font-size: 16px; font-weight: bold; } @@ -35,17 +35,24 @@

          Python Social Auth

          {% for name, backend in sublist %} {% associated backend %} {% if association %} -
          {% csrf_token %} + {% csrf_token %} Disconnect {{ backend|backend_name }}
          {% else %} - - - {{ backend|backend_name }} - + {% if name == "google-plus" %} +
          + + {{ backend|backend_name }} +
          + {% else %} + + + {{ backend|backend_name }} + + {% endif %} {% endif %} {% endfor %}
          @@ -77,7 +84,7 @@

          Python Social Auth

          - + Logout @@ -322,38 +329,57 @@
          {% if plus_id %} -
          {% csrf_token %} - - - -
          - -
          -
          + + {% endif %} - - {% endif %} * Logging out From 97d006bd9377e1db078a3e1c8f51b1f584dc3b8b Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Tue, 1 Sep 2015 18:00:39 +0200 Subject: [PATCH 677/890] Add REDIRECT_STATE = False --- social/backends/shopify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 861b1a6e7..a04b51284 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -19,6 +19,7 @@ class ShopifyOAuth2(BaseOAuth2): ('website', 'website'), ('expires', 'expires') ] + REDIRECT_STATE = False @property def shopifyAPI(self): From a93a07f78b1c939cfbcee9212ac02d508b04ebdf Mon Sep 17 00:00:00 2001 From: Michael Willmott Date: Wed, 2 Sep 2015 11:33:41 +0100 Subject: [PATCH 678/890] Added justgiving.com OAuth2 backend Signed-off-by: Michael Willmott --- docs/backends/justgiving.rst | 20 ++++++++++++ social/backends/justgiving.py | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 docs/backends/justgiving.rst create mode 100644 social/backends/justgiving.py diff --git a/docs/backends/justgiving.rst b/docs/backends/justgiving.rst new file mode 100644 index 000000000..52ca30139 --- /dev/null +++ b/docs/backends/justgiving.rst @@ -0,0 +1,20 @@ +Just Giving +=========== + +OAuth2 +------ + +Add the Just Giving OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.justgiving.JustGivingOAuth2', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_JUSTGIVING_KEY = '' + SOCIAL_AUTH_JUSTGIVING_SECRET = '' + +.. _Just Giving API Docs: https://api.justgiving.com/docs diff --git a/social/backends/justgiving.py b/social/backends/justgiving.py new file mode 100644 index 000000000..78f7fa214 --- /dev/null +++ b/social/backends/justgiving.py @@ -0,0 +1,57 @@ +from requests.auth import HTTPBasicAuth +from social.utils import handle_http_errors +from social.backends.oauth import BaseOAuth2 + + +class JustGivingOAuth2(BaseOAuth2): + """Just Giving OAuth authentication backend""" + name = 'justgiving' + ID_KEY = 'userId' + AUTHORIZATION_URL = 'https://identity.justgiving.com/connect/authorize' + ACCESS_TOKEN_URL = 'https://identity.justgiving.com/connect/token' + ACCESS_TOKEN_METHOD = 'POST' + USER_DATA_URL = 'https://api.justgiving.com/v1/account' + DEFAULT_SCOPE = ['openid', 'account', 'profile', 'email', 'fundraise'] + + def get_user_details(self, response): + """Return user details from Just Giving account""" + fullname, first_name, last_name = self.get_user_names( + '', + response.get('firstName'), + response.get('lastName')) + return { + 'username': response.get('email'), + 'email': response.get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + key, secret = self.get_key_and_secret() + return self.get_json( + self.USER_DATA_URL, + headers={ + 'Authorization': 'Bearer {0}'.format(access_token), + 'Content-Type': 'application/json', + 'x-application-key': secret, + 'x-api-key': key + }) + + @handle_http_errors + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + state = self.validate_state() + self.process_error(self.data) + + key, secret = self.get_key_and_secret() + response = self.request_access_token( + self.access_token_url(), + data=self.auth_complete_params(state), + headers=self.auth_headers(), + auth=HTTPBasicAuth(key, secret), + method=self.ACCESS_TOKEN_METHOD + ) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, *args, **kwargs) \ No newline at end of file From 0548524631a7a73d4493de618d2095a0591f3923 Mon Sep 17 00:00:00 2001 From: Bulgantamir Gankhuyag Date: Mon, 7 Sep 2015 18:24:02 +0900 Subject: [PATCH 679/890] added AuthUnreachableProvider exception to documentation --- docs/exceptions.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/exceptions.rst b/docs/exceptions.rst index 7fbd23768..fdbfa17c8 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -49,4 +49,7 @@ than just the ``ValueError`` usually raised. ``AuthTokenRevoked`` Raised when the user revoked the access_token in the provider. +``AuthUnreachableProvider`` + Raised when server couldn't communicate with backend. + These are a subclass of ``ValueError`` to keep backward compatibility. From bfeb34d0aaef686ea70d627087ff547b5f5d13b6 Mon Sep 17 00:00:00 2001 From: alrusdi Date: Tue, 15 Sep 2015 12:59:14 +0500 Subject: [PATCH 680/890] VK API workflow fix if error happens on vk-side --- social/backends/vk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/vk.py b/social/backends/vk.py index 1b567bf40..da1e19e28 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -111,7 +111,7 @@ def user_data(self, access_token, *args, **kwargs): 'fields': fields, }) - if data.get('error'): + if data and data.get('error'): error = data['error'] msg = error.get('error_msg', 'Unknown error') if error.get('error_code') == 5: @@ -122,7 +122,7 @@ def user_data(self, access_token, *args, **kwargs): if data: data = data.get('response')[0] data['user_photo'] = data.get('photo') # Backward compatibility - return data + return data or {} class VKAppOAuth2(VKOAuth2): From c310299cad6f0646f2cb61d0bb122bd304f19351 Mon Sep 17 00:00:00 2001 From: James Maddox Date: Wed, 23 Sep 2015 18:30:05 -0700 Subject: [PATCH 681/890] added Python 3 support to FacebookAppOAuth2's load_signed_request method --- social/backends/facebook.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index b887d11e6..f76b502c9 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -177,7 +177,7 @@ def auth_html(self): def load_signed_request(self, signed_request): def base64_url_decode(data): data = data.encode('ascii') - data += '=' * (4 - (len(data) % 4)) + data += '='.encode('ascii') * (4 - (len(data) % 4)) return base64.urlsafe_b64decode(data) key, secret = self.get_key_and_secret() @@ -187,8 +187,10 @@ def base64_url_decode(data): pass # ignore if can't split on dot else: sig = base64_url_decode(sig) - data = json.loads(base64_url_decode(payload)) - expected_sig = hmac.new(secret, msg=payload, + payload_json_bytes = base64_url_decode(payload) + data = json.loads(payload_json_bytes.decode('utf-8', 'replace')) + expected_sig = hmac.new(secret.encode('ascii'), + msg=payload.encode('ascii'), digestmod=hashlib.sha256).digest() # allow the signed_request to function for upto 1 day if constant_time_compare(sig, expected_sig) and \ From aa66b83d4848c09defa57384529eca83597a3692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 25 Sep 2015 17:56:14 -0300 Subject: [PATCH 682/890] v0.2.13 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 73c43df11..6294763db 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 12) +version = (0, 2, 13) extra = '' __version__ = '.'.join(map(str, version)) + extra From 3e5d77f38d1105da8a62451290a89a957a5bb981 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 27 Sep 2015 18:01:14 -0700 Subject: [PATCH 683/890] Fitbit OAuth 2.0 support --- README.rst | 2 +- docs/backends/fitbit.rst | 39 ++++++++++++---- docs/intro.rst | 2 +- .../local_settings.py.template | 2 +- examples/django_example/example/settings.py | 2 +- .../django_me_example/example/settings.py | 2 +- examples/flask_example/settings.py | 2 +- examples/flask_me_example/settings.py | 2 +- examples/pyramid_example/example/settings.py | 2 +- examples/tornado_example/settings.py | 2 +- examples/webpy_example/app.py | 2 +- social/backends/fitbit.py | 44 +++++++++++++++++-- social/tests/backends/test_fitbit.py | 2 +- 13 files changed, 82 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 03df9cd74..ab96b65cd 100644 --- a/README.rst +++ b/README.rst @@ -71,7 +71,7 @@ or current ones extended): * Exacttarget OAuth2 * Facebook_ OAuth2 and OAuth2 for Applications * Fedora_ OpenId http://fedoraproject.org/wiki/OpenID - * Fitbit_ OAuth1 + * Fitbit_ OAuth2 and OAuth1 * Flickr_ OAuth1 * Foursquare_ OAuth2 * `Google App Engine`_ Auth diff --git a/docs/backends/fitbit.rst b/docs/backends/fitbit.rst index 4218f6af3..8bd3389d6 100644 --- a/docs/backends/fitbit.rst +++ b/docs/backends/fitbit.rst @@ -1,15 +1,38 @@ Fitbit ====== -Fitbit offers OAuth1 as their auth mechanism. In order to enable it, follow: +Fitbit supports both OAuth 2.0 and OAuth 1.0a logins. +OAuth 2 is preferred for new integrations, as OAuth 1.0a does not support getting heartrate or location and will be deprecated in the future. -- Register a new application at `Fitbit dev portal`_, be sure to select - ``Browser`` as the application type. Set the ``Callback URL`` to - ``http:////complete/fitbit/``. +1. Register a new OAuth Consumer `here`_ -- Fill **Consumer Key** and **Consumer Secret** values:: +2. Configure the appropriate settings for OAuth 2.0 or OAuth 1.0a (see below). - SOCIAL_AUTH_FITBIT_KEY = '' - SOCIAL_AUTH_FITBIT_SECRET = '' +OAuth 2.0 or OAuth 1.0a +----------------------- -.. _Fitbit dev portal: https://dev.fitbit.com/apps/new +- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: + + SOCIAL_AUTH_FITBIT_KEY = '' + SOCIAL_AUTH_FITBIT_SECRET = '' + +OAuth 2.0 specific settings +--------------------------- + +By default, only the ``profile`` scope is requested. To request more scopes, set SOCIAL_AUTH_FITBIT_SCOPE:: + + SOCIAL_AUTH_FITBIT_SCOPE = [ + 'activity', + 'heartrate', + 'location', + 'nutrition', + 'profile', + 'settings', + 'sleep', + 'social', + 'weight' + ] + +The above will request all permissions from the user. + +.. _here: https://dev.fitbit.com/apps/new diff --git a/docs/intro.rst b/docs/intro.rst index b138974dc..747e22cdd 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -45,7 +45,7 @@ or extend current one): * Dropbox_ OAuth1 * Evernote_ OAuth1 * Facebook_ OAuth2 and OAuth2 for Applications - * Fitbit_ OAuth1 + * Fitbit_ OAuth2 and OAuth1 * Flickr_ OAuth1 * Foursquare_ OAuth2 * `Google App Engine`_ Auth diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template index e9fe9c0ef..98c8f8f26 100644 --- a/examples/cherrypy_example/local_settings.py.template +++ b/examples/cherrypy_example/local_settings.py.template @@ -26,7 +26,7 @@ SOCIAL_SETTINGS = { 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 58ab9c720..a22562bae 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -141,7 +141,7 @@ 'social.backends.facebook.FacebookAppOAuth2', 'social.backends.facebook.FacebookOAuth2', 'social.backends.fedora.FedoraOpenId', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.foursquare.FoursquareOAuth2', 'social.backends.github.GithubOAuth2', diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 82fd1142f..7dcdfad24 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -154,7 +154,7 @@ 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 8e0df5ef0..0abaa915c 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -37,7 +37,7 @@ 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py index 26d9c5c41..e4f2338ac 100644 --- a/examples/flask_me_example/settings.py +++ b/examples/flask_me_example/settings.py @@ -42,7 +42,7 @@ 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index 224917f6c..35a69ade1 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -31,7 +31,7 @@ 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index e7e474215..ff6b14684 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -30,7 +30,7 @@ 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 47d76c495..4109fac46 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -43,7 +43,7 @@ 'social.backends.dropbox.DropboxOAuth', 'social.backends.eveonline.EVEOnlineOAuth2', 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth', + 'social.backends.fitbit.FitbitOAuth2', 'social.backends.flickr.FlickrOAuth', 'social.backends.livejournal.LiveJournalOpenId', 'social.backends.soundcloud.SoundcloudOAuth2', diff --git a/social/backends/fitbit.py b/social/backends/fitbit.py index 655a711e5..16c4f77d2 100644 --- a/social/backends/fitbit.py +++ b/social/backends/fitbit.py @@ -1,12 +1,14 @@ """ -Fitbit OAuth1 backend, docs at: +Fitbit OAuth backend, docs at: http://psa.matiasaguirre.net/docs/backends/fitbit.html """ -from social.backends.oauth import BaseOAuth1 +import base64 +from social.backends.oauth import BaseOAuth1, BaseOAuth2 -class FitbitOAuth(BaseOAuth1): - """Fitbit OAuth authentication backend""" + +class FitbitOAuth1(BaseOAuth1): + """Fitbit OAuth1 authentication backend""" name = 'fitbit' AUTHORIZATION_URL = 'https://www.fitbit.com/oauth/authorize' REQUEST_TOKEN_URL = 'https://api.fitbit.com/oauth/request_token' @@ -26,3 +28,37 @@ def user_data(self, access_token, *args, **kwargs): 'https://api.fitbit.com/1/user/-/profile.json', auth=self.oauth_auth(access_token) )['user'] + +class FitbitOAuth2(BaseOAuth2): + """Fitbit OAuth2 authentication backend""" + name = 'fitbit' + AUTHORIZATION_URL = 'https://www.fitbit.com/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://api.fitbit.com/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + REFRESH_TOKEN_URL = 'https://api.fitbit.com/oauth2/token' + DEFAULT_SCOPE = ['profile'] + ID_KEY = 'encodedId' + REDIRECT_STATE = False + EXTRA_DATA = [('expires_in', 'expires'), + ('refresh_token', 'refresh_token', True), + ('encodedId', 'id'), + ('displayName', 'username')] + + def get_user_details(self, response): + """Return user details from Fitbit account""" + return {'username': response.get('displayName'), + 'email': ''} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + auth_header = {"Authorization": "Bearer %s" % access_token} + return self.get_json( + 'https://api.fitbit.com/1/user/-/profile.json', + headers=auth_header + )['user'] + def auth_headers(self): + return { + 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( + ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) + )) + } \ No newline at end of file diff --git a/social/tests/backends/test_fitbit.py b/social/tests/backends/test_fitbit.py index e12bdaa32..ae047309c 100644 --- a/social/tests/backends/test_fitbit.py +++ b/social/tests/backends/test_fitbit.py @@ -5,7 +5,7 @@ class FitbitOAuth1Test(OAuth1Test): - backend_path = 'social.backends.fitbit.FitbitOAuth' + backend_path = 'social.backends.fitbit.FitbitOAuth1' expected_username = 'foobar' access_token_body = urlencode({ 'oauth_token_secret': 'a-secret', From 737b32ff949fcd5713045bd08ce5f7b1d95072e6 Mon Sep 17 00:00:00 2001 From: Luke Briner Date: Mon, 28 Sep 2015 16:11:09 +0100 Subject: [PATCH 684/890] Update URLs to match new site and remove OAuth comment. --- docs/backends/pixelpin.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backends/pixelpin.rst b/docs/backends/pixelpin.rst index b623dcb4e..5a3c1310c 100644 --- a/docs/backends/pixelpin.rst +++ b/docs/backends/pixelpin.rst @@ -1,12 +1,12 @@ PixelPin ======== -PixelPin itself supports OAuth 1 and 2 but this provider only supports OAuth2. +PixelPin only supports OAuth2. PixelPin OAuth2 --------------- -Developer documentation for PixelPin can be found at https://login.pixelpin.co.uk/Developer/Index.aspx +Developer documentation for PixelPin can be found at http://developer.pixelpin.co.uk/ To setup OAuth2 do the following: - Register a new developer account at `PixelPin Developers`_. @@ -27,4 +27,4 @@ To setup OAuth2 do the following: .. _PixelPin homepage: http://pixelpin.co.uk/ .. _PixelPin Account Page: https://login.pixelpin.co.uk/ -.. _PixelPin Developers: https://login.pixelpin.co.uk/Developers/Index.aspx +.. _PixelPin Developers: http://developer.pixelpin.co.uk/ From 79bc6ffd7dea17bba363a1727d2fc67701bdb60b Mon Sep 17 00:00:00 2001 From: Maarten van Schaik Date: Tue, 29 Sep 2015 16:44:27 +0200 Subject: [PATCH 685/890] Save extra_data on login --- social/storage/sqlalchemy_orm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 71010f74d..621bf4c1f 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -12,6 +12,7 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.types import PickleType, Text from sqlalchemy.schema import UniqueConstraint +from sqlalchemy.ext.mutable import MutableDict from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage @@ -66,7 +67,7 @@ class SQLAlchemyUserMixin(SQLAlchemyMixin, UserMixin): __table_args__ = (UniqueConstraint('provider', 'uid'),) id = Column(Integer, primary_key=True) provider = Column(String(32)) - extra_data = Column(JSONType) + extra_data = Column(MutableDict.as_mutable(JSONType)) uid = None user_id = None user = None From 15d75dad20af14ae6772b6a70375b2728de1015a Mon Sep 17 00:00:00 2001 From: Nick Catalano Date: Wed, 30 Sep 2015 02:25:57 -0500 Subject: [PATCH 686/890] Add ability to store arbitrary user fields in test user model --- social/tests/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/social/tests/models.py b/social/tests/models.py index d5d193c2c..d142d9260 100644 --- a/social/tests/models.py +++ b/social/tests/models.py @@ -24,7 +24,7 @@ class User(BaseModel): cache = {} _is_active = True - def __init__(self, username, email=None): + def __init__(self, username, email=None, **extra_user_fields): self.id = User.next_id() self.username = username self.email = email @@ -32,6 +32,7 @@ def __init__(self, username, email=None): self.slug = None self.social = [] self.extra_data = {} + self.extra_user_fields = extra_user_fields self.save() def is_active(self): @@ -100,8 +101,8 @@ def user_exists(cls, username): return User.cache.get(username) is not None @classmethod - def create_user(cls, username, email=None): - return User(username=username, email=email) + def create_user(cls, username, email=None, **extra_user_fields): + return User(username=username, email=email, **extra_user_fields) @classmethod def get_user(cls, pk): From 3c83f4406f4754e0a8eab3d773e305d2724608ce Mon Sep 17 00:00:00 2001 From: Maarten van Schaik Date: Fri, 2 Oct 2015 12:15:08 +0200 Subject: [PATCH 687/890] Store all tokens on refresh --- social/storage/base.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/social/storage/base.py b/social/storage/base.py index 475b70cf2..1964bc1a2 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -52,14 +52,7 @@ def refresh_token(self, strategy, *args, **kwargs): if token and backend and hasattr(backend, 'refresh_token'): backend = backend(strategy=strategy) response = backend.refresh_token(token, *args, **kwargs) - access_token = response.get('access_token') - refresh_token = response.get('refresh_token') - - if access_token or refresh_token: - if access_token: - self.extra_data['access_token'] = access_token - if refresh_token: - self.extra_data['refresh_token'] = refresh_token + if self.set_extra_data(response): self.save() def expiration_datetime(self): From dae128023443a683f4cd24c0d0f984114e19ab2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 4 Oct 2015 23:04:30 -0300 Subject: [PATCH 688/890] Swtich travisci from legacy to container infrastructure --- .travis.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d9380b2ce..9cfd567ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +sudo: false env: global: - REQUIREMENTS=requirements.txt @@ -17,9 +18,11 @@ matrix: env: - REQUIREMENTS=requirements-python3.txt - TEST_REQUIREMENTS=social/tests/requirements-python3.txt -before_install: - - sudo apt-get update -qq - - sudo apt-get install -y libxmlsec1-dev swig + addons: +apt: + packages: + - libxmlsec1-dev + - swig install: - "python setup.py -q install" - "travis_retry pip install -r $REQUIREMENTS" From 920427c2834c13c09e23d1bdbd66646e1f7a51cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 4 Oct 2015 23:06:27 -0300 Subject: [PATCH 689/890] Syntax typo --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9cfd567ab..1d819220a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ matrix: env: - REQUIREMENTS=requirements-python3.txt - TEST_REQUIREMENTS=social/tests/requirements-python3.txt - addons: apt: packages: - libxmlsec1-dev From 67919ec9e2b7d80805a6fa3614d051a7d38713d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 4 Oct 2015 23:08:33 -0300 Subject: [PATCH 690/890] Fix travis conf --- .travis.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d819220a..705c3cdd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,16 +12,17 @@ matrix: include: - python: "3.3" env: - - REQUIREMENTS=requirements-python3.txt - - TEST_REQUIREMENTS=social/tests/requirements-python3.txt + - REQUIREMENTS=requirements-python3.txt + - TEST_REQUIREMENTS=social/tests/requirements-python3.txt - python: "3.4" env: - - REQUIREMENTS=requirements-python3.txt - - TEST_REQUIREMENTS=social/tests/requirements-python3.txt -apt: - packages: - - libxmlsec1-dev - - swig + - REQUIREMENTS=requirements-python3.txt + - TEST_REQUIREMENTS=social/tests/requirements-python3.txt +addons: + apt: + packages: + - libxmlsec1-dev + - swig install: - "python setup.py -q install" - "travis_retry pip install -r $REQUIREMENTS" From ce3da987c83d89cfc77df3256eee67f436394d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 09:49:14 -0300 Subject: [PATCH 691/890] Fix pypy support, no python-saml for it, silence python2.6 warnings --- .gitignore | 2 ++ social/tests/__init__.py | 8 ++++++++ social/tests/requirements-pypy.txt | 8 ++++++++ tox.ini | 3 +++ 4 files changed, 21 insertions(+) create mode 100644 social/tests/requirements-pypy.txt diff --git a/.gitignore b/.gitignore index e6c34a6e0..f6bf7759d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ changelog.sh .DS_Store .\#* \#*\# + +.python-version diff --git a/social/tests/__init__.py b/social/tests/__init__.py index e69de29bb..3dee9751d 100644 --- a/social/tests/__init__.py +++ b/social/tests/__init__.py @@ -0,0 +1,8 @@ +import sys +import warnings + + +# Ignore deprecation warnings on Python2.6. Maybe it's time to ditch this +# oldie? +if sys.version_info[0] == 2 and sys.version_info[1] == 6: + warnings.filterwarnings('ignore', category=Warning) diff --git a/social/tests/requirements-pypy.txt b/social/tests/requirements-pypy.txt new file mode 100644 index 000000000..33cef2575 --- /dev/null +++ b/social/tests/requirements-pypy.txt @@ -0,0 +1,8 @@ +httpretty==0.6.5 +coverage>=3.6 +mock==1.0.1 +nose>=1.2.1 +rednose>=0.4.1 +requests>=1.1.0 +PyJWT>=1.0.0,<2.0.0 +unittest2==0.5.1 diff --git a/tox.ini b/tox.ini index 298244e4d..57eec3416 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,9 @@ envlist = py26, py27, py33, py34, pypy, doc commands = nosetests --where=social/tests --stop deps = -r{toxinidir}/social/tests/requirements.txt +[testenv:pypy] +deps = -r{toxinidir}/social/tests/requirements-pypy.txt + [testenv:py33] deps = -r{toxinidir}/social/tests/requirements-python3.txt From 18e9c7c4c8cded86d18c3feed8ee8bd0b51e02f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 10:14:00 -0300 Subject: [PATCH 692/890] Pypy test requirements for travisci --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 705c3cdd4..b3d3b275c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,9 @@ python: - "pypy" matrix: include: + - python: "pypy" + env: + - TEST_REQUIREMENTS=social/tests/requirements-pypy.txt - python: "3.3" env: - REQUIREMENTS=requirements-python3.txt From 74c6616fee913ddd7929eda275d727b134e11850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 10:16:29 -0300 Subject: [PATCH 693/890] Remove duplicated pypy entry, add python 3.5 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b3d3b275c..87d86c18c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ env: python: - "2.6" - "2.7" - - "pypy" matrix: include: - python: "pypy" @@ -21,6 +20,10 @@ matrix: env: - REQUIREMENTS=requirements-python3.txt - TEST_REQUIREMENTS=social/tests/requirements-python3.txt + - python: "3.5" + env: + - REQUIREMENTS=requirements-python3.txt + - TEST_REQUIREMENTS=social/tests/requirements-python3.txt addons: apt: packages: From c4ea47ce54d0ce13f7e65637363645c7b8b84618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 10:29:52 -0300 Subject: [PATCH 694/890] Remove python3.5 (openid breaks), silence pypy warings --- .travis.yml | 4 ---- social/tests/__init__.py | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87d86c18c..fc67088c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,6 @@ matrix: env: - REQUIREMENTS=requirements-python3.txt - TEST_REQUIREMENTS=social/tests/requirements-python3.txt - - python: "3.5" - env: - - REQUIREMENTS=requirements-python3.txt - - TEST_REQUIREMENTS=social/tests/requirements-python3.txt addons: apt: packages: diff --git a/social/tests/__init__.py b/social/tests/__init__.py index 3dee9751d..db503e8f8 100644 --- a/social/tests/__init__.py +++ b/social/tests/__init__.py @@ -4,5 +4,6 @@ # Ignore deprecation warnings on Python2.6. Maybe it's time to ditch this # oldie? -if sys.version_info[0] == 2 and sys.version_info[1] == 6: +if sys.version_info[0] == 2 and sys.version_info[1] == 6 or + hasattr(sys, 'pypy_version_info'): warnings.filterwarnings('ignore', category=Warning) From 7f74771b02cbb470e1c5d086f8a72fdb61dc3694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 10:34:58 -0300 Subject: [PATCH 695/890] Fix syntax error --- social/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/tests/__init__.py b/social/tests/__init__.py index db503e8f8..46c7ced67 100644 --- a/social/tests/__init__.py +++ b/social/tests/__init__.py @@ -4,6 +4,6 @@ # Ignore deprecation warnings on Python2.6. Maybe it's time to ditch this # oldie? -if sys.version_info[0] == 2 and sys.version_info[1] == 6 or +if sys.version_info[0] == 2 and sys.version_info[1] == 6 or \ hasattr(sys, 'pypy_version_info'): warnings.filterwarnings('ignore', category=Warning) From cad745c83a2d22e828e98b7bbf39dde01a9c37b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 10:50:28 -0300 Subject: [PATCH 696/890] Codestyle and missing docs --- docs/backends/justgiving.rst | 5 ++++- social/backends/justgiving.py | 17 ++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/backends/justgiving.rst b/docs/backends/justgiving.rst index 52ca30139..9a5fd6ea6 100644 --- a/docs/backends/justgiving.rst +++ b/docs/backends/justgiving.rst @@ -4,7 +4,10 @@ Just Giving OAuth2 ------ -Add the Just Giving OAuth2 backend to your settings page:: +Follow the steps at `Just Giving API Docs`_ to register your +application and get the needed keys. + +- Add the Just Giving OAuth2 backend to your settings page:: SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( ... diff --git a/social/backends/justgiving.py b/social/backends/justgiving.py index 78f7fa214..e5ced3eba 100644 --- a/social/backends/justgiving.py +++ b/social/backends/justgiving.py @@ -30,14 +30,12 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" key, secret = self.get_key_and_secret() - return self.get_json( - self.USER_DATA_URL, - headers={ - 'Authorization': 'Bearer {0}'.format(access_token), - 'Content-Type': 'application/json', - 'x-application-key': secret, - 'x-api-key': key - }) + return self.get_json(self.USER_DATA_URL, headers={ + 'Authorization': 'Bearer {0}'.format(access_token), + 'Content-Type': 'application/json', + 'x-application-key': secret, + 'x-api-key': key + }) @handle_http_errors def auth_complete(self, *args, **kwargs): @@ -54,4 +52,5 @@ def auth_complete(self, *args, **kwargs): method=self.ACCESS_TOKEN_METHOD ) self.process_error(response) - return self.do_auth(response['access_token'], response=response, *args, **kwargs) \ No newline at end of file + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) From 04a074263a7905fb36cc898444a72134d641c400 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Wed, 16 Sep 2015 10:03:54 +0200 Subject: [PATCH 697/890] Add signed request signature for Instagram --- social/backends/instagram.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index 509aa010f..5c84211cc 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -2,6 +2,8 @@ Instagram OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/instagram.html """ +from hashlib import sha256 +import hmac from social.backends.oauth import BaseOAuth2 @@ -35,5 +37,15 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" + key, secret = self.get_key_and_secret() + params = params={'access_token': access_token} + sig = self._generate_sig("users/self", params, secret) + params['sig'] = sig return self.get_json('https://api.instagram.com/v1/users/self', - params={'access_token': access_token}) + params) + + def _generate_sig(self, endpoint, params, secret): + sig = endpoint + for key in sorted(params.keys()): + sig += '|%s=%s' % (key, params[key]) + return hmac.new(secret.encode(), sig.encode(), sha256).hexdigest() From 720aff5ea3ba6ef5fa83a972742bfd619b731f8c Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Wed, 16 Sep 2015 10:13:28 +0200 Subject: [PATCH 698/890] Fix typo --- social/backends/instagram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index 5c84211cc..a76fac003 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -38,7 +38,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" key, secret = self.get_key_and_secret() - params = params={'access_token': access_token} + params = {'access_token': access_token} sig = self._generate_sig("users/self", params, secret) params['sig'] = sig return self.get_json('https://api.instagram.com/v1/users/self', From 2a2e76d9ce8708041ea8576e32f6a06a5ac9b2b6 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Wed, 16 Sep 2015 10:27:40 +0200 Subject: [PATCH 699/890] Fix kwarg --- social/backends/instagram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index a76fac003..b1d9872be 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -42,7 +42,7 @@ def user_data(self, access_token, *args, **kwargs): sig = self._generate_sig("users/self", params, secret) params['sig'] = sig return self.get_json('https://api.instagram.com/v1/users/self', - params) + params=params) def _generate_sig(self, endpoint, params, secret): sig = endpoint From d42fa2531b7ca330977647c2b438e798f94034fd Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Wed, 16 Sep 2015 10:35:19 +0200 Subject: [PATCH 700/890] Fix missing slash --- social/backends/instagram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index b1d9872be..8c2f32f7e 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -39,7 +39,7 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" key, secret = self.get_key_and_secret() params = {'access_token': access_token} - sig = self._generate_sig("users/self", params, secret) + sig = self._generate_sig("/users/self", params, secret) params['sig'] = sig return self.get_json('https://api.instagram.com/v1/users/self', params=params) From c4b9c51a2dafb0a6c243ff1653b297d3ba5be90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 11:01:42 -0300 Subject: [PATCH 701/890] Link justgiving doc --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 799ca6238..a6b63daeb 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -81,6 +81,7 @@ Social backends google instagram jawbone + justgiving kakao khanacademy lastfm From dead1da75566fccde9e2ec325d69644caf1900bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 5 Oct 2015 11:03:17 -0300 Subject: [PATCH 702/890] Switch import order --- social/backends/instagram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index 8c2f32f7e..d67134adf 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -2,8 +2,10 @@ Instagram OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/instagram.html """ -from hashlib import sha256 import hmac + +from hashlib import sha256 + from social.backends.oauth import BaseOAuth2 From 48bd44587e74121c58b3cdb20b7678fb600001a9 Mon Sep 17 00:00:00 2001 From: Sergey Trofimov Date: Wed, 7 Oct 2015 18:58:57 +0300 Subject: [PATCH 703/890] Update vk.py Fix for modern VK API (5.34 tested) where no uid param in response, and fallback to get_user_id from BaseAuth class which gets uid by self.ID_KEY --- social/backends/vk.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/social/backends/vk.py b/social/backends/vk.py index da1e19e28..db388d294 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -85,9 +85,6 @@ class VKOAuth2(BaseOAuth2): ('expires_in', 'expires') ] - def get_user_id(self, details, response): - return response['uid'] - def get_user_details(self, response): """Return user details from VK.com account""" fullname, first_name, last_name = self.get_user_names( From b96be11c37cf26bc52d11011f5c742e1c39f87ab Mon Sep 17 00:00:00 2001 From: Sergey Trofimov Date: Wed, 7 Oct 2015 19:10:21 +0300 Subject: [PATCH 704/890] Update odnoklassniki.py Fix scope separator which is ";" in OK api Fix logic of get_user_details, it will correctly process email in OK response (because OK may return email in response in case you have permissions for that and scope "GET_EMAIL") --- social/backends/odnoklassniki.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 89cec2dc7..1a7cf267c 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -15,6 +15,7 @@ class OdnoklassnikiOAuth2(BaseOAuth2): name = 'odnoklassniki-oauth2' ID_KEY = 'uid' ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ';' AUTHORIZATION_URL = 'http://www.odnoklassniki.ru/oauth/authorize' ACCESS_TOKEN_URL = 'http://api.odnoklassniki.ru/oauth/token.do' EXTRA_DATA = [('refresh_token', 'refresh_token'), @@ -29,7 +30,7 @@ def get_user_details(self, response): ) return { 'username': response['uid'], - 'email': '', + 'email': response.get('email', ''), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name From 7c3f0e2eca4444761d241de124a9dd9899519c31 Mon Sep 17 00:00:00 2001 From: Peter Schmidt Date: Mon, 12 Oct 2015 16:58:15 +1100 Subject: [PATCH 705/890] Fix a few typos in backends --- social/backends/base.py | 4 ++-- social/backends/evernote.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/social/backends/base.py b/social/backends/base.py index f55e8e377..059fe4c9b 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -117,7 +117,7 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): return out def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return deafault extra data to store in extra_data field""" + """Return default extra data to store in extra_data field""" data = {} for entry in (self.EXTRA_DATA or []) + self.setting('EXTRA_DATA', []): if not isinstance(entry, (list, tuple)): @@ -194,7 +194,7 @@ def continue_pipeline(self, *args, **kwargs): def auth_extra_arguments(self): """Return extra arguments needed on auth process. The defaults can be - overriden by GET parameters.""" + overridden by GET parameters.""" extra_arguments = self.setting('AUTH_EXTRA_ARGUMENTS', {}).copy() extra_arguments.update((key, self.data[key]) for key in extra_arguments if key in self.data) diff --git a/social/backends/evernote.py b/social/backends/evernote.py index 10abef7ed..2fde3e5cb 100644 --- a/social/backends/evernote.py +++ b/social/backends/evernote.py @@ -57,7 +57,7 @@ def access_token(self, token): def extra_data(self, user, uid, response, details=None, *args, **kwargs): data = super(EvernoteOAuth, self).extra_data(user, uid, response, details, *args, **kwargs) - # Evernote returns expiration timestamp in miliseconds, so it needs to + # Evernote returns expiration timestamp in milliseconds, so it needs to # be normalized. if 'expires' in data: data['expires'] = int(data['expires']) / 1000 From 8f615fa4216c3f4933d3613a8ca303473825be1a Mon Sep 17 00:00:00 2001 From: Kevin Harvey Date: Fri, 16 Oct 2015 10:15:28 -0500 Subject: [PATCH 706/890] Fixes a few grammar issues in the docs --- docs/configuration/django.rst | 9 ++++----- docs/configuration/index.rst | 4 ++-- docs/configuration/settings.rst | 27 ++++++++++++++------------- docs/index.rst | 2 +- docs/logging_out.rst | 10 +++++----- docs/pipeline.rst | 6 +++--- docs/strategies.rst | 2 +- docs/use_cases.rst | 13 ++++++------- 8 files changed, 36 insertions(+), 37 deletions(-) diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst index b093434a5..7aeef15be 100644 --- a/docs/configuration/django.rst +++ b/docs/configuration/django.rst @@ -38,14 +38,14 @@ Database (For Django 1.7 and higher) sync database to create needed models:: - ./manage.py makemigrations + ./manage.py migrate If you're still using South, you'll need override SOUTH_MIGRATION_MODULES_:: SOUTH_MIGRATION_MODULES = { 'default': 'social.apps.django_app.default.south_migrations' } - + Note that Django's app labels take the last part of the import, so in this case ``social.apps.django_app.default`` becomes ``default`` here. @@ -148,9 +148,8 @@ A base middleware is provided that handles ``SocialAuthBaseException`` by providing a message to the user via the Django messages framework, and then responding with a redirect to a URL defined in one of the middleware methods. -The middleware is at ``social.apps.django_app.middleware.SocialAuthExceptionMiddleware``. -Any method can be overrided but for simplifications these two are the -recommended:: +The middleware is at ``social.apps.django_app.middleware.SocialAuthExceptionMiddleware``. +Any method can be overridden, but for simplicity these two are recommended:: get_message(request, exception) get_redirect_uri(request, exception) diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst index 3cfa6b735..92dc283f3 100644 --- a/docs/configuration/index.rst +++ b/docs/configuration/index.rst @@ -4,8 +4,8 @@ Configuration All the apps share the settings names, some settings for Django framework are special (like ``AUTHENTICATION_BACKENDS``). -Below there's a main settings document detailing the configuration and they -purpose, plus sections detailed for each framework and they particularities. +Below there's a main settings document detailing each configuration and its +purpose, plus sections detailed for each framework and their particularities. Support for more frameworks will be added in the future, pull-requests are very welcome. diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 0db8bb84d..2824bb451 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -76,7 +76,7 @@ results and others for error situations. ``SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/new-users-redirect-url/'`` Used to redirect new registered users, will be used in place of - ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` if defined. Note that ``?next=/foo`` is appended if present, + ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` if defined. Note that ``?next=/foo`` is appended if present, if you want new users to go to next, you'll need to do it yourself. ``SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/new-association-redirect-url/'`` @@ -114,9 +114,9 @@ model lacks them a ``True`` value is assumed. Tweaking some fields length --------------------------- -Some databases impose limitations to indexes columns (like MySQL InnoDB), these +Some databases impose limitations on index columns (like MySQL InnoDB). These limitations won't play nice on some ``UserSocialAuth`` fields. To avoid such -error define some of the following settings. +errors, define some of the following settings. ``SOCIAL_AUTH_UID_LENGTH = `` Used to define the max length of the field `uid`. A value of 223 should work @@ -136,12 +136,12 @@ error define some of the following settings. Username generation ------------------- -Some providers return an username, others just an Id or email or first and last +Some providers return a username, others just an ID or email or first and last names. The application tries to build a meaningful username when possible but defaults to generating one if needed. -An UUID is appended to usernames in case of collisions. Here are some settings -to control usernames generation. +A UUID is appended to usernames in case of collisions. Here are some settings +to control username generation. ``SOCIAL_AUTH_UUID_LENGTH = 16`` This controls the length of the UUID appended to usernames. @@ -205,21 +205,22 @@ For OAuth backends:: Processing redirects and urlopen -------------------------------- -The application issues several redirects and API calls, this following settings +The application issues several redirects and API calls. The following settings allow some tweaks to the behavior of these. ``SOCIAL_AUTH_SANITIZE_REDIRECTS = False`` The auth process finishes with a redirect, by default it's done to the value of ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` but can be overridden with - ``next`` GET argument. If this settings is ``True``, this application will - very the domain of the final URL and only redirect to it if it's on the + ``next`` GET argument. If this setting is ``True``, this application will + vary the domain of the final URL and only redirect to it if it's on the same domain. - + ``SOCIAL_AUTH_REDIRECT_IS_HTTPS = False`` On projects behind a reverse proxy that uses HTTPS, the redirect URIs - can became with the wrong schema (``http://`` instead of ``https://``) when - the request lacks some headers, and might cause errors with the auth - process, to force HTTPS in the final URIs set this setting to ``True`` + can have the wrong schema (``http://`` instead of ``https://``) if + the request lacks the appropriate headers, which might cause errors during + the auth process. To force HTTPS in the final URIs set this setting to + ``True`` ``SOCIAL_AUTH_URLOPEN_TIMEOUT = 30`` Any ``urllib2.urlopen`` call will be performed with the default timeout diff --git a/docs/index.rst b/docs/index.rst index ca1506921..473de706b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ authorization mechanism for Python projects supporting protocols like OAuth (1 and 2), OpenId and others. The initial codebase is derived from django-social-auth_ with the idea of -generalizing the process to suite the different frameworks around, providing +generalizing the process to suit the different frameworks around, providing the needed tools to bring support to new frameworks. django-social-auth_ itself was a product of modified code from diff --git a/docs/logging_out.rst b/docs/logging_out.rst index aed9ed663..bd10bd86e 100644 --- a/docs/logging_out.rst +++ b/docs/logging_out.rst @@ -1,13 +1,13 @@ Disconnect and Logging Out ========================== -It's a common misconception that ``disconnect`` action is the same as logging -the user out, but is far from it. +It's a common misconception that the ``disconnect`` action is the same as +logging the user out, but this is not the case. -``Disconnect`` is the way that your users have to say to you "forget about my -account", that implies removing the ``UserSocialAuth`` instance that was +``Disconnect`` is the way that your users can ask your project to "forget about +my account". This implies removing the ``UserSocialAuth`` instance that was created, this also implies that the user won't be able to login back into your -site with the social account, instead the action will be a signup, a new user +site with the social account. Instead the action will be a signup, a new user instance will be created, not related to the previous one. Logging out is just a way to say "forget my current session", and usually diff --git a/docs/pipeline.rst b/docs/pipeline.rst index b22987aa4..715abf48e 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -75,8 +75,8 @@ The default pipeline is composed by:: ) -It's possible to override it by defining the setting ``SOCIAL_AUTH_PIPELINE``, -for example a pipeline that won't create users, just accept already registered +It's possible to override it by defining the setting ``SOCIAL_AUTH_PIPELINE``. +For example, a pipeline that won't create users, just accept already registered ones would look like this:: SOCIAL_AUTH_PIPELINE = ( @@ -178,7 +178,7 @@ There's a pipeline to validate email addresses, but it relies a lot on your project. The pipeline is at ``social.pipeline.mail.mail_validation`` and it's a partial -pipeline, it will return a redirect to an URL that you can use to tell the +pipeline, it will return a redirect to a URL that you can use to tell the users that an email validation was sent to them. If you want to mention the email address you can get it from the session under the key ``email_validation_address``. diff --git a/docs/strategies.rst b/docs/strategies.rst index 0a2086fde..72e014e5c 100644 --- a/docs/strategies.rst +++ b/docs/strategies.rst @@ -8,7 +8,7 @@ capabilities under a common API to reuse as much code as possible. Description ----------- -An strategy responsibility is to provide access to: +A strategy's responsibility is to provide access to: * Request data and host information and URI building * Session access diff --git a/docs/use_cases.rst b/docs/use_cases.rst index bebb60a81..3ac58560f 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -7,13 +7,12 @@ Some miscellaneous options and use cases for python-social-auth_. Return the user to the original page ------------------------------------ -There's a common scenario where it's desired to return the user back to the -original page from where it was requested to login. For that purpose, the usual -``next`` query-string argument is used, the value of this parameter will be -stored in the session and later used to redirect the user when login was -successful. +There's a common scenario to return the user back to the original page from +where they requested to login. For that purpose, the usual ``next`` query-string +argument is used. The value of this parameter will be stored in the session and +later used to redirect the user when login was successful. -In order to use it just define it with your link, for instance, when using +In order to use it, just define it with your link. For instance, when using Django:: Login with Facebook @@ -155,7 +154,7 @@ At the moment python-social-auth_ doesn't provide a method to define multiple scopes for single backend, this is usually desired since it's recommended to ask the user for the minimum scope possible and increase the access when it's really needed. It's possible to add a new backend extending the original one to -accomplish that behavior, there are two ways to do it. +accomplish that behavior. There are two ways to do it. 1. Overriding ``get_scope()`` method:: From 53c24c846aee5bbbfe5969d727441fd9b0d53fe3 Mon Sep 17 00:00:00 2001 From: lneoe Date: Wed, 14 Oct 2015 16:02:36 +0800 Subject: [PATCH 707/890] use `openid` as username --- docs/backends/qq.rst | 6 +++++- social/backends/qq.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/backends/qq.rst b/docs/backends/qq.rst index 2f3a932d3..97b7486c3 100644 --- a/docs/backends/qq.rst +++ b/docs/backends/qq.rst @@ -22,6 +22,10 @@ QQ implemented OAuth2 protocol for their authentication mechanism. To enable The values for ``nickname``, ``figureurl_qq_1`` and ``gender`` will be stored in the ``extra_data`` field. The ``nickname`` will be used as the account -username. ``figureurl_qq_1`` can be used as the profile image. +username. ``figureurl_qq_1`` can be used as the profile image. sometimes +nickname will duplicate with another qq account, to avoid this issue it's +possible to use ``openid`` as ``username`` by define this setting:: + + SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME = True .. _QQ: http://connect.qq.com/ diff --git a/social/backends/qq.py b/social/backends/qq.py index 0a50df77a..4894b6016 100644 --- a/social/backends/qq.py +++ b/social/backends/qq.py @@ -25,8 +25,26 @@ class QQOAuth2(BaseOAuth2): ] def get_user_details(self, response): + """ + Return user detail from QQ account + sometimes nickname will duplicate with another qq account, to avoid + this issue it's possible to use `openid` as `username` by define + `SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME = True` + """ + if self.setting('SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME', False): + username = response.get('openid', '') + else: + username = response.get('nickname', '') + + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('nickname', '') + ) + return { - 'username': response.get('nickname', '') + 'username': username, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name } def get_openid(self, access_token): From 5059b0509ed5a839711e0475b608fdd629d370a7 Mon Sep 17 00:00:00 2001 From: lneoe Date: Sat, 17 Oct 2015 12:44:57 +0800 Subject: [PATCH 708/890] use `self.setting('USE_OPENID_AS_USERNAME', False)` this will fast --- social/backends/qq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/qq.py b/social/backends/qq.py index 4894b6016..55c41f964 100644 --- a/social/backends/qq.py +++ b/social/backends/qq.py @@ -31,7 +31,7 @@ def get_user_details(self, response): this issue it's possible to use `openid` as `username` by define `SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME = True` """ - if self.setting('SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME', False): + if self.setting('USE_OPENID_AS_USERNAME', False): username = response.get('openid', '') else: username = response.get('nickname', '') From 4f1e66eb733747f0013f25a71347ada7d1ff19b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Prunell?= Date: Tue, 20 Oct 2015 15:09:08 -0200 Subject: [PATCH 709/890] Fix typo --- social/backends/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index a73508a1b..b6d7abbc0 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -371,7 +371,7 @@ def process_error(self, data): @handle_http_errors def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" + """Completes login process, must return user instance""" state = self.validate_state() self.process_error(self.data) From 1194e6f7370285739c1c220673c4cb2b6ecc6657 Mon Sep 17 00:00:00 2001 From: Chun-Jung Lee Date: Thu, 22 Oct 2015 03:34:18 +0800 Subject: [PATCH 710/890] Support all kind of data type (like uuid) of User.id on Pyramid --- social/apps/pyramid_app/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py index c38bf00b0..2d70e0681 100644 --- a/social/apps/pyramid_app/models.py +++ b/social/apps/pyramid_app/models.py @@ -31,7 +31,7 @@ def _session(cls): class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin): """Social Auth association model""" uid = Column(String(UID_LENGTH)) - user_id = Column(Integer, ForeignKey(User.id), + user_id = Column(User.id.type, ForeignKey(User.id), nullable=False, index=True) user = relationship(User, backref=backref('social_auth', lazy='dynamic')) From 41551fc1af6d8dff896460657a51b8e729a599a6 Mon Sep 17 00:00:00 2001 From: opaqe Date: Sat, 24 Oct 2015 07:34:54 -0400 Subject: [PATCH 711/890] Fixed 401 client redirect error for reddit backend --- social/backends/.reddit.py.swp | Bin 0 -> 12288 bytes social/backends/reddit.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 social/backends/.reddit.py.swp diff --git a/social/backends/.reddit.py.swp b/social/backends/.reddit.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..ee5262440b855e77069f633858a24096e3f82a76 GIT binary patch literal 12288 zcmeI2O>f*p7{{j^5O1Lo2nlf-!ePAW#+$SuphyvN6Ng5myOGyhRaBKTYtLrgcx^K? z4un<82S9=g7Y-aa!Ub{Vg!&oa#GBy8cPNNw#@>xLtw^q*vGk8UGtcwPJimESqD+7J z*3M1*hP{g5nMdfe&T}tWN@y;#{2aUcI$$e5mcuMv5?L72 zWQi7X*ebtUeHtFJG!iY&q9~Sjetb?u(`*PB0v94s$idQ^bLiUDm1SlB+QO^&#TR-P zay3SVfFWQA7y^cXAz%m?0)~Jg@c$qn^GoO>Nacwto!6`Pr8DoUXkLbZAz%m?0)~Jg zU@LHnQp z^cH9y^zeCvz5^YBdZ25dmqCBeA@mFA0q7gh=b$mD2Wo)+dJdtVKtF*X^c zluMB_Zh89VW5YyhzzW1~jV#M-hs)B8p{ zOIZ^yQhp%dwRrfx`f9Ah?@85jGW}8&Wj|Guev$F=$@+NZgxG_OkEpa3b;<`!28Zl8 zplK8c76LV^(Xd$>W)ZVyqY#y2#^VpEjI)%i<371gMI2&+ui!ovY;Dypcp~V41^uu5F`Y8W(Msf9!LCl2=QaWM)oscFb39VPm=Bm618__tKSPzOM zIrTL<$*kX<4$LC#gB-S}2x6EKNTw{2(=}ni2Ci)%Wi(q+8?;4Wo<}T&bxvX7 zE8E$M8QUaI4^Itir5P&?1_DjV&9ugm>I8mPweD@-bAm0$ci_~xqtooV?X95Kb-loO z+im+$@AHD`rY(2d>FxSKceAtSDhG%JqmSh=DG}+0pzH2Ap5u2sFdP1ad{5wqeU?b^-%?flfI^| z7G(%qN8tLQZNVYNN Date: Mon, 26 Oct 2015 17:09:07 +0500 Subject: [PATCH 712/890] Add pinterest backend --- README.rst | 2 + docs/backends/index.rst | 1 + docs/backends/pinterest.rst | 29 ++++++++++++++ docs/intro.rst | 2 + social/backends/pinterest.py | 51 +++++++++++++++++++++++++ social/tests/backends/test_pinterest.py | 49 ++++++++++++++++++++++++ 6 files changed, 134 insertions(+) create mode 100644 docs/backends/pinterest.rst create mode 100644 social/backends/pinterest.py create mode 100644 social/tests/backends/test_pinterest.py diff --git a/README.rst b/README.rst index 03df9cd74..c15752d9f 100644 --- a/README.rst +++ b/README.rst @@ -97,6 +97,7 @@ or current ones extended): * OpenId_ * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth * OpenSuse_ OpenId http://en.opensuse.org/openSUSE:Connect + * Pinterest_ OAuth2 * PixelPin_ OAuth2 * Pocket_ OAuth2 * Podio_ OAuth2 @@ -317,3 +318,4 @@ check `django-social-auth LICENSE`_ for details: .. _requests: http://docs.python-requests.org/en/latest/ .. _PixelPin: http://pixelpin.co.uk .. _Zotero: http://www.zotero.org/ +.. _Pinterest: https://www.pinterest.com diff --git a/docs/backends/index.rst b/docs/backends/index.rst index a6b63daeb..2734078f9 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -103,6 +103,7 @@ Social backends openstreetmap orbi persona + pinterest pixelpin pocket podio diff --git a/docs/backends/pinterest.rst b/docs/backends/pinterest.rst new file mode 100644 index 000000000..d55be1c2f --- /dev/null +++ b/docs/backends/pinterest.rst @@ -0,0 +1,29 @@ +Pinterest +========= + +Pinterest implemented OAuth2 protocol for their authentication mechanism. +To enable ``python-social-auth`` support follow this steps: + +1. Go to `Pinterest developers zone`_ and create an application. + +2. Fill App Id and Secret in your project settings:: + + SOCIAL_AUTH_PINTEREST_KEY = '...' + SOCIAL_AUTH_PINTEREST_SECRET = '...' + SOCIAL_AUTH_PINTEREST_SCOPE = [ + 'read_public', + 'write_public', + 'read_relationships', + 'write_relationships' + ] + +3. Enable the backend:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.pinterest.PinterestOAuth2', + ... + ) + +.. _Pinterest Apps Console: https://developers.pinterest.com/apps/ +.. _Pinterest Documentation: https://developers.pinterest.com/docs/ diff --git a/docs/intro.rst b/docs/intro.rst index b138974dc..4f1aa773e 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -64,6 +64,7 @@ or extend current one): * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ * Podio_ OAuth2 + * Pinterest_ OAuth2 * Rdio_ OAuth1 and OAuth2 * Readability_ OAuth1 * Shopify_ OAuth2 @@ -161,6 +162,7 @@ section. .. _Yahoo: http://yahoo.com .. _Yammer: https://www.yammer.com .. _Yandex: https://yandex.ru +.. _Pinterest: https://www.pinterest.com .. _Readability: http://www.readability.com/ .. _Stackoverflow: http://stackoverflow.com/ .. _Steam: http://steamcommunity.com/ diff --git a/social/backends/pinterest.py b/social/backends/pinterest.py new file mode 100644 index 000000000..3a907034c --- /dev/null +++ b/social/backends/pinterest.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +Pinterest OAuth2 backend, docs at: + https://developers.pinterest.com/docs/api/authentication/ +""" + +from __future__ import unicode_literals + +import ssl + +from social.backends.oauth import BaseOAuth2 + + +class PinterestOAuth2(BaseOAuth2): + name = 'pinterest' + ID_KEY = 'user_id' + AUTHORIZATION_URL = 'https://api.pinterest.com/oauth/' + ACCESS_TOKEN_URL = 'https://api.pinterest.com/v1/oauth/token' + REDIRECT_STATE = False + ACCESS_TOKEN_METHOD = 'POST' + SSL_PROTOCOL = ssl.PROTOCOL_TLSv1 + FAKE_HTTPS = False + + def get_redirect_uri(self, state=None): + url = super(PinterestOAuth2, self).get_redirect_uri(state) + return self.FAKE_HTTPS and url.replace('http:', 'https:') or url + + def user_data(self, access_token, *args, **kwargs): + response = self.get_json('https://api.pinterest.com/v1/me/', + params={'access_token': access_token}) + + if 'data' in response: + username = response['data']['url'].strip('/').split('/')[-1] + response = { + 'user_id': response['data']['id'], + 'first_name': response['data']['first_name'], + 'last_name': response['data']['last_name'], + 'username': username, + } + return response + + def get_user_details(self, response): + fullname, first_name, last_name = self.get_user_names( + first_name=response['first_name'], + last_name=response['last_name']) + + return {'username': response.get('username'), + 'email': None, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} diff --git a/social/tests/backends/test_pinterest.py b/social/tests/backends/test_pinterest.py new file mode 100644 index 000000000..8f61e5d95 --- /dev/null +++ b/social/tests/backends/test_pinterest.py @@ -0,0 +1,49 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class PinterestOAuth2Test(OAuth2Test): + backend_path = 'social.backends.pinterest.PinterestOAuth2' + user_data_url = 'https://api.pinterest.com/v1/me/' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'id': '4788400174839062', + 'first_name': 'Foo', + 'last_name': 'Bar', + 'username': 'foobar', + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() + + +class PinterestOAuth2BrokenServerResponseTest(OAuth2Test): + backend_path = 'social.backends.pinterest.PinterestOAuth2' + user_data_url = 'https://api.pinterest.com/v1/me/' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'data': { + 'id': '4788400174839062', + 'first_name': 'Foo', + 'last_name': 'Bar', + 'url': 'https://www.pinterest.com/foobar/', + } + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From fcd11bf96e6ef2966bb1436df7f405e7144c0863 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 11 Nov 2015 15:58:48 -0500 Subject: [PATCH 713/890] Formatter fixes for SAML to support Py2.6 --- social/backends/saml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index 198a57451..206b26c6f 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -278,7 +278,7 @@ def get_user_id(self, details, response): """ idp = self.get_idp(response['idp_name']) uid = idp.get_user_permanent_id(response['attributes']) - return '{}:{}'.format(idp.name, uid) + return '{0}:{1}'.format(idp.name, uid) def auth_complete(self, *args, **kwargs): """ @@ -293,7 +293,7 @@ def auth_complete(self, *args, **kwargs): if errors or not auth.is_authenticated(): reason = auth.get_last_error_reason() raise AuthFailed( - self, 'SAML login failed: {} ({})'.format(errors, reason) + self, 'SAML login failed: {0} ({1})'.format(errors, reason) ) attributes = auth.get_attributes() From 9e653b2b72d2bf39d92e7d1d36a14cfb2b249435 Mon Sep 17 00:00:00 2001 From: Sergey Petrov Date: Thu, 12 Nov 2015 13:48:51 +0300 Subject: [PATCH 714/890] Remove unused response arg from user_data method of yandex backend --- social/backends/yandex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/yandex.py b/social/backends/yandex.py index 144ee5cd7..ab53366f2 100644 --- a/social/backends/yandex.py +++ b/social/backends/yandex.py @@ -48,7 +48,7 @@ def get_user_details(self, response): 'first_name': first_name, 'last_name': last_name} - def user_data(self, access_token, response, *args, **kwargs): + def user_data(self, access_token, *args, **kwargs): return self.get_json('https://login.yandex.ru/info', params={'oauth_token': access_token, 'format': 'json'}) @@ -72,7 +72,7 @@ def get_user_details(self, response): 'first_name': first_name, 'last_name': last_name} - def user_data(self, access_token, response, *args, **kwargs): + def user_data(self, access_token, *args, **kwargs): return self.get_json('https://login.yandex.ru/info', params={'oauth_token': access_token, 'format': 'json'}) From ce2e3a32dcca5fad603bd46a183b66a856006984 Mon Sep 17 00:00:00 2001 From: SeokJun Hong Date: Mon, 16 Nov 2015 09:37:11 -0500 Subject: [PATCH 715/890] Add naver backends --- docs/backends/naver.rst | 25 ++++++++++++++ social/backends/naver.py | 53 +++++++++++++++++++++++++++++ social/tests/backends/test_naver.py | 35 +++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 docs/backends/naver.rst create mode 100644 social/backends/naver.py create mode 100644 social/tests/backends/test_naver.py diff --git a/docs/backends/naver.rst b/docs/backends/naver.rst new file mode 100644 index 000000000..18bd99c5d --- /dev/null +++ b/docs/backends/naver.rst @@ -0,0 +1,25 @@ +Naver +======== + +Naver uses OAuth v2 for Authentication. + +- Register a new application at the `Naver API`_, and + +- add naver oauth backend to your settings page: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.naver.NaverOAuth2', + ... + ) + +- fill ``Client ID`` and ``Client Secret`` from developer.naver.com values in the settings:: + + SOCIAL_AUTH_NAVER_KEY = '' + SOCIAL_AUTH_NAVER_SECRET = '' + +- you can get EXTRA_DATA: + + SOCIAL_AUTH_NAVER_EXTRA_DATA = ['nickname', 'gender', 'age', 'birthday', 'profile_image'] + +.. _Naver API: https://nid.naver.com/devcenter/docs.nhn?menu=API diff --git a/social/backends/naver.py b/social/backends/naver.py new file mode 100644 index 000000000..df67586a2 --- /dev/null +++ b/social/backends/naver.py @@ -0,0 +1,53 @@ + +from xml.dom import minidom + +from social.backends.oauth import BaseOAuth2 + +class NaverOAuth2(BaseOAuth2): + """Naver OAuth authentication backend""" + name = 'naver' + AUTHORIZATION_URL = 'https://nid.naver.com/oauth2.0/authorize' + ACCESS_TOKEN_URL = 'https://nid.naver.com/oauth2.0/token' + ACCESS_TOKEN_METHOD = 'POST' + EXTRA_DATA = [ + ('id', 'id'), + ] + + def get_user_id(self, details, response): + return response.get('id') + + def get_user_details(self, response): + """Return user details from Naver account""" + return { + 'username': response.get('username'), + 'email': response.get('email'), + 'fullname': response.get('username'), + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + dom = minidom.parseString(self.request( + 'https://openapi.naver.com/v1/nid/getUserProfile.xml', + method='POST', + headers={'Authorization': 'Bearer {0}'.format(access_token)} + ).text.encode('utf-8').strip()) + + return { + 'id': dom.getElementsByTagName('id')[0].childNodes[0].data, + 'email': dom.getElementsByTagName('email')[0].childNodes[0].data, + 'username': dom.getElementsByTagName('name')[0].childNodes[0].data, + 'nickname': dom.getElementsByTagName('nickname')[0].childNodes[0].data, + 'gender': dom.getElementsByTagName('gender')[0].childNodes[0].data, + 'age': dom.getElementsByTagName('age')[0].childNodes[0].data, + 'birthday': dom.getElementsByTagName('birthday')[0].childNodes[0].data, + 'profile_image': dom.getElementsByTagName('profile_image')[0].childNodes[0].data, + } + + def auth_headers(self): + client_id, client_secret = self.get_key_and_secret() + return { + 'grant_type': 'authorization_code', + 'code': self.data.get('code'), + 'client_id': client_id, + 'client_secret': client_secret, + } diff --git a/social/tests/backends/test_naver.py b/social/tests/backends/test_naver.py new file mode 100644 index 000000000..d625046bf --- /dev/null +++ b/social/tests/backends/test_naver.py @@ -0,0 +1,35 @@ +import json +from social.tests.backends.oauth import OAuth2Test + +class NaverOAuth2Test(OAuth2Test): + backend_path = 'social.backends.naver.NaverOAuth2' + user_data_url = 'https://openapi.naver.com/v1/nid/getUserProfile.xml' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer', + }) + + user_data_content_type = 'text/xml' + user_data_body = \ + '' \ + '' \ + '00' \ + 'success' \ + '' \ + '' \ + 'naverIDLogin' \ + 'userName' \ + '123456' \ + 'M' \ + '40-49' \ + '01-01' \ + 'http://naver.com/image.url.jpg' \ + '' \ + '' + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() \ No newline at end of file From 5a7d61c7099cd852fa33768fbbafba98c6ead25b Mon Sep 17 00:00:00 2001 From: SeokJun Hong Date: Mon, 16 Nov 2015 09:45:42 -0500 Subject: [PATCH 716/890] fix a typing error in naver.rst --- docs/backends/naver.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backends/naver.rst b/docs/backends/naver.rst index 18bd99c5d..3389f36e1 100644 --- a/docs/backends/naver.rst +++ b/docs/backends/naver.rst @@ -5,7 +5,7 @@ Naver uses OAuth v2 for Authentication. - Register a new application at the `Naver API`_, and -- add naver oauth backend to your settings page: +- add naver oauth backend to your settings page:: SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( ... @@ -18,7 +18,7 @@ Naver uses OAuth v2 for Authentication. SOCIAL_AUTH_NAVER_KEY = '' SOCIAL_AUTH_NAVER_SECRET = '' -- you can get EXTRA_DATA: +- you can get EXTRA_DATA:: SOCIAL_AUTH_NAVER_EXTRA_DATA = ['nickname', 'gender', 'age', 'birthday', 'profile_image'] From 2c491c6b7f6fc58a4b9cab42a0eb8e6aa11a9842 Mon Sep 17 00:00:00 2001 From: SeokJun Hong Date: Tue, 17 Nov 2015 09:14:31 -0500 Subject: [PATCH 717/890] fix Travis-CI failed --- social/backends/naver.py | 5 ++--- social/tests/backends/test_naver.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/social/backends/naver.py b/social/backends/naver.py index df67586a2..709db94a4 100644 --- a/social/backends/naver.py +++ b/social/backends/naver.py @@ -27,9 +27,8 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" dom = minidom.parseString(self.request( - 'https://openapi.naver.com/v1/nid/getUserProfile.xml', - method='POST', - headers={'Authorization': 'Bearer {0}'.format(access_token)} + 'https://openapi.naver.com/v1/nid/getUserProfile.xml', + headers={'Authorization': 'Bearer {0}'.format(access_token), 'Content_Type': 'text/xml'} ).text.encode('utf-8').strip()) return { diff --git a/social/tests/backends/test_naver.py b/social/tests/backends/test_naver.py index d625046bf..fcb0a676e 100644 --- a/social/tests/backends/test_naver.py +++ b/social/tests/backends/test_naver.py @@ -12,22 +12,25 @@ class NaverOAuth2Test(OAuth2Test): user_data_content_type = 'text/xml' user_data_body = \ + '' \ '' \ '' \ '00' \ 'success' \ '' \ '' \ - 'naverIDLogin' \ - 'userName' \ - '123456' \ + '' \ + '' \ + '' \ + '' \ 'M' \ - '40-49' \ - '01-01' \ - 'http://naver.com/image.url.jpg' \ + '' \ + '' \ + '' \ '' \ '' + def test_login(self): self.do_login() From 27a6bca417c2abc27643eea3c61c13d2aa4f2a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 23 Nov 2015 18:38:36 -0300 Subject: [PATCH 718/890] Add instagram authentication backend to docs --- docs/backends/instagram.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/backends/instagram.rst b/docs/backends/instagram.rst index 34163eb80..ccbcf1d42 100644 --- a/docs/backends/instagram.rst +++ b/docs/backends/instagram.rst @@ -5,6 +5,14 @@ Instagram uses OAuth v2 for Authentication. - Register a new application at the `Instagram API`_, and +- Add instagram backend to ``AUTHENTICATION_SETTINGS``:: + + AUTHENTICATION_SETTINGS = ( + ... + 'social.backends.insagram.InstagramOAuth2', + ... + ) + - fill ``Client Id`` and ``Client Secret`` values in the settings:: SOCIAL_AUTH_INSTAGRAM_KEY = '' From d47d4dbe59b0a47ed7f231992d01b165f6a4053b Mon Sep 17 00:00:00 2001 From: James Keys Date: Tue, 1 Dec 2015 10:51:15 +0700 Subject: [PATCH 719/890] Update settings.rst For the django strategy at least, the AUTH_EXTRA_ARGUMENTS settings still need to be prefixed with SOCIAL_AUTH_ to be recognized. --- docs/configuration/settings.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 0db8bb84d..3841fa357 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -176,12 +176,12 @@ example to request Facebook to show Mobile authorization page, define:: For other providers, just define settings in the form:: - _AUTH_EXTRA_ARGUMENTS = {...} + SOCIAL_AUTH__AUTH_EXTRA_ARGUMENTS = {...} Also, you can send extra parameters on request token process by defining settings in the same way explained above but with this other suffix:: - _REQUEST_TOKEN_EXTRA_ARGUMENTS = {...} + SOCIAL_AUTH__REQUEST_TOKEN_EXTRA_ARGUMENTS = {...} Basic information is requested to the different providers in order to create a coherent user instance (with first and last name, email and full name), this From 8977cf773479414bce0501ff6a48f5b1f61abe56 Mon Sep 17 00:00:00 2001 From: Romulo Tavares Date: Sat, 5 Dec 2015 12:35:24 -0200 Subject: [PATCH 720/890] Changed instagram backend to new authorization routes --- social/backends/instagram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/instagram.py b/social/backends/instagram.py index d67134adf..494d8e2a0 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -11,8 +11,8 @@ class InstagramOAuth2(BaseOAuth2): name = 'instagram' - AUTHORIZATION_URL = 'https://instagram.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://instagram.com/oauth/access_token' + AUTHORIZATION_URL = 'https://api.instagram.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://api.instagram.com/oauth/access_token' ACCESS_TOKEN_METHOD = 'POST' def get_user_id(self, details, response): From 52a33a9324b1be4343fd1f61420a010ff5289d48 Mon Sep 17 00:00:00 2001 From: Eric Carmichael Date: Tue, 8 Dec 2015 05:02:20 -0800 Subject: [PATCH 721/890] get more data from battlenet --- social/backends/battlenet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py index ecf77a1bb..eefd2fa5f 100644 --- a/social/backends/battlenet.py +++ b/social/backends/battlenet.py @@ -45,6 +45,6 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """ Loads user data from service """ return self.get_json( - 'https://eu.api.battle.net/account/user/battletag', + 'https://eu.api.battle.net/account/user', params={'access_token': access_token} ) From 7013a389cd88bdfe893a5efa6d5856a589b61e31 Mon Sep 17 00:00:00 2001 From: Eric Carmichael Date: Tue, 8 Dec 2015 05:08:15 -0800 Subject: [PATCH 722/890] revise info for account id and new https requirement --- docs/backends/battlenet.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/backends/battlenet.rst b/docs/backends/battlenet.rst index 65a1ac08b..db6a842ee 100644 --- a/docs/backends/battlenet.rst +++ b/docs/backends/battlenet.rst @@ -19,12 +19,16 @@ enable ``python-social-auth`` support follow this steps: ... ) -Note: The API returns an accountId which will be used as identifier for the -user. If you want to allow the user to choose a username from his own +Note: If you want to allow the user to choose a username from his own characters, some further steps are required, see the use cases part of the -documentation. +documentation. To get the account id and battletag use the user_data function, as +`account id is no longer passed inherently`_. + +Another note: If you get a 500 response "Internal Server Error" the API now requires `https on callback endpoints`_. Further documentation at `Developer Guide`_. .. _Battlenet Developer Portal: https://dev.battle.net/ .. _Developer Guide: https://dev.battle.net/docs/read/oauth +.. _https on callback endpoints: http://us.battle.net/en/forum/topic/17085510584 +.. _account id is no longer passed inherently: http://us.battle.net/en/forum/topic/18300183303 From b3642603f6170592f9a6994261852622314a4de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 8 Dec 2015 13:21:51 -0300 Subject: [PATCH 723/890] Typos and PEP8 --- docs/backends/qq.rst | 7 ++++--- social/backends/qq.py | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/backends/qq.rst b/docs/backends/qq.rst index 97b7486c3..f0c1ef64a 100644 --- a/docs/backends/qq.rst +++ b/docs/backends/qq.rst @@ -22,9 +22,10 @@ QQ implemented OAuth2 protocol for their authentication mechanism. To enable The values for ``nickname``, ``figureurl_qq_1`` and ``gender`` will be stored in the ``extra_data`` field. The ``nickname`` will be used as the account -username. ``figureurl_qq_1`` can be used as the profile image. sometimes -nickname will duplicate with another qq account, to avoid this issue it's -possible to use ``openid`` as ``username`` by define this setting:: +username. ``figureurl_qq_1`` can be used as the profile image. + +Sometimes nickname will duplicate with another ``qq`` account, to avoid this +issue it's possible to use ``openid`` as ``username`` by define this setting:: SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME = True diff --git a/social/backends/qq.py b/social/backends/qq.py index 55c41f964..34f36b321 100644 --- a/social/backends/qq.py +++ b/social/backends/qq.py @@ -26,10 +26,9 @@ class QQOAuth2(BaseOAuth2): def get_user_details(self, response): """ - Return user detail from QQ account - sometimes nickname will duplicate with another qq account, to avoid - this issue it's possible to use `openid` as `username` by define - `SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME = True` + Return user detail from QQ account sometimes nickname will duplicate + with another qq account, to avoid this issue it's possible to use + openid as username. """ if self.setting('USE_OPENID_AS_USERNAME', False): username = response.get('openid', '') From c6445dd198aa6f584bc1a991ff358a5326dea9b1 Mon Sep 17 00:00:00 2001 From: falcon1kr Date: Tue, 8 Dec 2015 17:23:24 -0800 Subject: [PATCH 724/890] Update social_auth.py --- social/pipeline/social_auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 87895ec10..697bf1a00 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -17,6 +17,7 @@ def auth_allowed(backend, details, response, *args, **kwargs): def social_user(backend, uid, user=None, *args, **kwargs): provider = backend.name + new_association = True social = backend.strategy.storage.user.get_social_auth(provider, uid) if social: if user and social.user != user: @@ -24,10 +25,11 @@ def social_user(backend, uid, user=None, *args, **kwargs): raise AuthAlreadyAssociated(backend, msg) elif not user: user = social.user + new_association = False return {'social': social, 'user': user, 'is_new': user is None, - 'new_association': False} + 'new_association': new_association} def associate_user(backend, uid, user=None, social=None, *args, **kwargs): From e73089dc5965ba2e5995744f5cd35f7ce5b04232 Mon Sep 17 00:00:00 2001 From: falcon1kr Date: Tue, 8 Dec 2015 17:23:24 -0800 Subject: [PATCH 725/890] fixing new_association returned by social.pipeline.social_auth.social_user --- social/pipeline/social_auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 87895ec10..697bf1a00 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -17,6 +17,7 @@ def auth_allowed(backend, details, response, *args, **kwargs): def social_user(backend, uid, user=None, *args, **kwargs): provider = backend.name + new_association = True social = backend.strategy.storage.user.get_social_auth(provider, uid) if social: if user and social.user != user: @@ -24,10 +25,11 @@ def social_user(backend, uid, user=None, *args, **kwargs): raise AuthAlreadyAssociated(backend, msg) elif not user: user = social.user + new_association = False return {'social': social, 'user': user, 'is_new': user is None, - 'new_association': False} + 'new_association': new_association} def associate_user(backend, uid, user=None, social=None, *args, **kwargs): From 63bcd3135d9655505717f598af366fba7348d062 Mon Sep 17 00:00:00 2001 From: falcon1kr Date: Tue, 8 Dec 2015 18:02:40 -0800 Subject: [PATCH 726/890] setting is_new value to False when associate_by_email found a user with corresponding email --- social/pipeline/social_auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index 697bf1a00..ff99404a0 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -78,7 +78,8 @@ def associate_by_email(backend, details, user=None, *args, **kwargs): 'The given email address is associated with another account' ) else: - return {'user': users[0]} + return {'user': users[0], + 'is_new': False} def load_extra_data(backend, details, response, uid, user, *args, **kwargs): From 6ae6454d8cb2e320f4864ddb45e9141996b3749f Mon Sep 17 00:00:00 2001 From: sushantgawali Date: Wed, 9 Dec 2015 17:43:36 +0530 Subject: [PATCH 727/890] Changed Azure AD endpoints according to new authorization flow --- social/backends/azuread.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/social/backends/azuread.py b/social/backends/azuread.py index a015a0fc9..4c5923508 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -39,8 +39,8 @@ class AzureADOAuth2(BaseOAuth2): name = 'azuread-oauth2' SCOPE_SEPARATOR = ' ' - AUTHORIZATION_URL = 'https://login.windows.net/common/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://login.windows.net/common/oauth2/token' + AUTHORIZATION_URL = 'https://login.microsoftonline.com/common/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/token' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False DEFAULT_SCOPE = ['openid', 'profile', 'user_impersonation'] @@ -101,6 +101,8 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): def refresh_token_params(self, token, *args, **kwargs): return { + 'client_id' : self.setting('KEY'), + 'client_secret' : self.setting('SECRET'), 'refresh_token': token, 'grant_type': 'refresh_token', 'resource': self.setting('RESOURCE') From a7b4c907d4d3aaa438801064573bb96000ab9626 Mon Sep 17 00:00:00 2001 From: sushantgawali Date: Tue, 15 Dec 2015 09:09:54 +0530 Subject: [PATCH 728/890] removed extra tabbing and spaces in azuread --- social/backends/azuread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/azuread.py b/social/backends/azuread.py index 4c5923508..3c2be305f 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -101,8 +101,8 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): def refresh_token_params(self, token, *args, **kwargs): return { - 'client_id' : self.setting('KEY'), - 'client_secret' : self.setting('SECRET'), + 'client_id': self.setting('KEY'), + 'client_secret': self.setting('SECRET'), 'refresh_token': token, 'grant_type': 'refresh_token', 'resource': self.setting('RESOURCE') From eca0f263e8a944a144a08f130e06aeb651e645b4 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Wed, 16 Dec 2015 22:00:26 +0200 Subject: [PATCH 729/890] Fix Django 1.10 deprecation warnings In django_app/urls.py: * Use a list instead of `patterns` * Use view callables instead of strings Fixes #804, #754 --- social/apps/django_app/urls.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/social/apps/django_app/urls.py b/social/apps/django_app/urls.py index 43a97aa8f..1d72e5b14 100644 --- a/social/apps/django_app/urls.py +++ b/social/apps/django_app/urls.py @@ -1,27 +1,28 @@ """URLs module""" from django.conf import settings try: - from django.conf.urls import patterns, url + from django.conf.urls import url except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, url + from django.conf.urls.defaults import url from social.utils import setting_name +from social.apps.django_app import views extra = getattr(settings, setting_name('TRAILING_SLASH'), True) and '/' or '' -urlpatterns = patterns('social.apps.django_app.views', +urlpatterns = [ # authentication / association - url(r'^login/(?P[^/]+){0}$'.format(extra), 'auth', + url(r'^login/(?P[^/]+){0}$'.format(extra), views.auth, name='begin'), - url(r'^complete/(?P[^/]+){0}$'.format(extra), 'complete', + url(r'^complete/(?P[^/]+){0}$'.format(extra), views.complete, name='complete'), # disconnection - url(r'^disconnect/(?P[^/]+){0}$'.format(extra), 'disconnect', + url(r'^disconnect/(?P[^/]+){0}$'.format(extra), views.disconnect, name='disconnect'), url(r'^disconnect/(?P[^/]+)/(?P[^/]+){0}$' - .format(extra), 'disconnect', name='disconnect_individual'), -) + .format(extra), views.disconnect, name='disconnect_individual'), +] From 345721dcbae2da0db5622893e907709b9e0a99be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 23 Dec 2015 13:18:37 -0300 Subject: [PATCH 730/890] PEP8 and doc styles applied --- docs/backends/index.rst | 1 + docs/backends/naver.rst | 12 ++++++----- social/backends/naver.py | 31 ++++++++++++++++++----------- social/tests/backends/test_naver.py | 9 ++++++--- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index a6b63daeb..3d6b7e613 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -99,6 +99,7 @@ Social backends moves naszaklasa nationbuilder + naver odnoklassnikiru openstreetmap orbi diff --git a/docs/backends/naver.rst b/docs/backends/naver.rst index 3389f36e1..60d21d40f 100644 --- a/docs/backends/naver.rst +++ b/docs/backends/naver.rst @@ -1,5 +1,5 @@ Naver -======== +===== Naver uses OAuth v2 for Authentication. @@ -13,13 +13,15 @@ Naver uses OAuth v2 for Authentication. ... ) -- fill ``Client ID`` and ``Client Secret`` from developer.naver.com values in the settings:: +- fill ``Client ID`` and ``Client Secret`` from developer.naver.com + values in the settings:: SOCIAL_AUTH_NAVER_KEY = '' SOCIAL_AUTH_NAVER_SECRET = '' -- you can get EXTRA_DATA:: - - SOCIAL_AUTH_NAVER_EXTRA_DATA = ['nickname', 'gender', 'age', 'birthday', 'profile_image'] +- you can get EXTRA_DATA:: + + SOCIAL_AUTH_NAVER_EXTRA_DATA = ['nickname', 'gender', 'age', + 'birthday', 'profile_image'] .. _Naver API: https://nid.naver.com/devcenter/docs.nhn?menu=API diff --git a/social/backends/naver.py b/social/backends/naver.py index 709db94a4..89acfdbb6 100644 --- a/social/backends/naver.py +++ b/social/backends/naver.py @@ -1,4 +1,3 @@ - from xml.dom import minidom from social.backends.oauth import BaseOAuth2 @@ -26,20 +25,25 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - dom = minidom.parseString(self.request( + response = self.request( 'https://openapi.naver.com/v1/nid/getUserProfile.xml', - headers={'Authorization': 'Bearer {0}'.format(access_token), 'Content_Type': 'text/xml'} - ).text.encode('utf-8').strip()) + headers={ + 'Authorization': 'Bearer {0}'.format(access_token), + 'Content_Type': 'text/xml' + } + ) + + dom = minidom.parseString(response.text.encode('utf-8').strip()) return { - 'id': dom.getElementsByTagName('id')[0].childNodes[0].data, - 'email': dom.getElementsByTagName('email')[0].childNodes[0].data, - 'username': dom.getElementsByTagName('name')[0].childNodes[0].data, - 'nickname': dom.getElementsByTagName('nickname')[0].childNodes[0].data, - 'gender': dom.getElementsByTagName('gender')[0].childNodes[0].data, - 'age': dom.getElementsByTagName('age')[0].childNodes[0].data, - 'birthday': dom.getElementsByTagName('birthday')[0].childNodes[0].data, - 'profile_image': dom.getElementsByTagName('profile_image')[0].childNodes[0].data, + 'id': self._dom_value(dom, 'id'), + 'email': self._dom_value(dom, 'email'), + 'username': self._dom_value(dom, 'name'), + 'nickname': self._dom_value(dom, 'nickname'), + 'gender': self._dom_value(dom, 'gender'), + 'age': self._dom_value(dom, 'age'), + 'birthday': self._dom_value(dom, 'birthday'), + 'profile_image': self._dom_value(dom, 'profile_image') } def auth_headers(self): @@ -50,3 +54,6 @@ def auth_headers(self): 'client_id': client_id, 'client_secret': client_secret, } + + def _dom_value(self, dom, key): + return dom.getElementsByTagName(key)[0].childNodes[0].data diff --git a/social/tests/backends/test_naver.py b/social/tests/backends/test_naver.py index fcb0a676e..096a3c2cf 100644 --- a/social/tests/backends/test_naver.py +++ b/social/tests/backends/test_naver.py @@ -1,6 +1,8 @@ import json + from social.tests.backends.oauth import OAuth2Test + class NaverOAuth2Test(OAuth2Test): backend_path = 'social.backends.naver.NaverOAuth2' user_data_url = 'https://openapi.naver.com/v1/nid/getUserProfile.xml' @@ -21,7 +23,9 @@ class NaverOAuth2Test(OAuth2Test): '' \ '' \ '' \ - '' \ + '' \ + '' \ + '' \ '' \ 'M' \ '' \ @@ -30,9 +34,8 @@ class NaverOAuth2Test(OAuth2Test): '' \ '' - def test_login(self): self.do_login() def test_partial_pipeline(self): - self.do_partial_pipeline() \ No newline at end of file + self.do_partial_pipeline() From f91455771cf5fdc0b4de4cc50e67362767ea6b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 23 Dec 2015 13:19:24 -0300 Subject: [PATCH 731/890] Document run_tox script dependencies --- run_tox.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/run_tox.sh b/run_tox.sh index ec3bbe0d9..b38f764e6 100755 --- a/run_tox.sh +++ b/run_tox.sh @@ -1,4 +1,18 @@ #!/bin/bash +# 1. Install pyenv +# 2. Install python versions +# pyenv install 2.6.9 +# pyenv install 2.7.11 +# pyenv install 3.3.6 +# pyenv install 3.4.4 +# pyenv install pypy-4.0.1 +# 3. Switch to each version and install / update setuptools, pip, tox +# pyenv local 2.6.9 +# pip install -U setuptools pip tox +# 4. Enable versions +# pyenv local 2.6.9 2.7.11 3.3.6 3.4.4 pypy-4.0.1 +# 5. Run tox + which pyenv && eval "$(pyenv init -)" tox From 75c10b70448df3a86cf7df9b8f0470990c2b6111 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Thu, 24 Dec 2015 15:03:03 -0600 Subject: [PATCH 732/890] Fix typo: "attacht he" -> "attach the" --- docs/backends/google.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/google.rst b/docs/backends/google.rst index 929c79fb1..e252d8895 100644 --- a/docs/backends/google.rst +++ b/docs/backends/google.rst @@ -83,7 +83,7 @@ auth process. ``CLIENT SECRET``. * Add the sign-in button to your template, you can use the SDK button - or add your own and attacht he click handler to it (check `Google+ Identity Sign-In`_ + or add your own and attach the click handler to it (check `Google+ Identity Sign-In`_ documentation about it)::
          Google+ Sign In
          From 5e3b710c0c0a17f4fa79529b02bc89cf29cde805 Mon Sep 17 00:00:00 2001 From: "Buddy Lindsey, Jr" Date: Sat, 26 Dec 2015 15:04:19 -0600 Subject: [PATCH 733/890] Add support for Drip Email Marketing Site --- docs/backends/drip.rst | 13 +++++++++++++ social/backends/drip.py | 25 +++++++++++++++++++++++++ social/tests/backends/test_drip.py | 25 +++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 docs/backends/drip.rst create mode 100644 social/backends/drip.py create mode 100644 social/tests/backends/test_drip.py diff --git a/docs/backends/drip.rst b/docs/backends/drip.rst new file mode 100644 index 000000000..b09c64e2b --- /dev/null +++ b/docs/backends/drip.rst @@ -0,0 +1,13 @@ +Drip +========= + +Drip uses OAuth v2 for Authentication. + +- Register a new application with `Drip`_, and + +- fill ``Client ID`` and ``Client Secret`` from getdrip.com values in the settings:: + + SOCIAL_AUTH_DRIP_KEY = '' + SOCIAL_AUTH_DRIP_SECRET = '' + +.. _Drip: https://www.getdrip.com/user/applications diff --git a/social/backends/drip.py b/social/backends/drip.py new file mode 100644 index 000000000..af1f6b3cf --- /dev/null +++ b/social/backends/drip.py @@ -0,0 +1,25 @@ +""" +Drip OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/drip.html +""" +from social.backends.oauth import BaseOAuth2 + + +class DripOAuth(BaseOAuth2): + name = 'drip' + AUTHORIZATION_URL = 'https://www.getdrip.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://www.getdrip.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + + def get_user_id(self, details, response): + return details['email'] + + def get_user_details(self, response): + return {'email': response['users'][0]['email'], + 'fullname': response['users'][0]['name'], + 'username': response['users'][0]['email']} + + def user_data(self, access_token, *args, **kwargs): + return self.get_json( + 'https://api.getdrip.com/v2/user', + headers={'Authorization': 'Bearer {}'.format(access_token)}) diff --git a/social/tests/backends/test_drip.py b/social/tests/backends/test_drip.py new file mode 100644 index 000000000..e9424acda --- /dev/null +++ b/social/tests/backends/test_drip.py @@ -0,0 +1,25 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class DripOAuthTest(OAuth2Test): + backend_path = 'social.backends.drip.DripOAuth' + user_data_url = 'https://api.getdrip.com/v2/user' + expected_username = 'other@example.com' + access_token_body = json.dumps({ + "access_token": "822bbf7cd12243df", + "token_type": "bearer", + "scope": "public" + }) + + user_data_body = json.dumps( + {'users': [ + {'email': 'other@example.com', 'name': None} + ]}) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 01e4ee459b331b1895639fe361f57a2d62c492d0 Mon Sep 17 00:00:00 2001 From: "Buddy Lindsey, Jr" Date: Sat, 26 Dec 2015 17:50:19 -0600 Subject: [PATCH 734/890] convert to useing 2.6 string replacement --- social/backends/drip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/drip.py b/social/backends/drip.py index af1f6b3cf..96ae93480 100644 --- a/social/backends/drip.py +++ b/social/backends/drip.py @@ -22,4 +22,4 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://api.getdrip.com/v2/user', - headers={'Authorization': 'Bearer {}'.format(access_token)}) + headers={'Authorization': 'Bearer %s' % access_token}) From 10a6f5570a4471d156c2bc06ecd4313189d23172 Mon Sep 17 00:00:00 2001 From: Jared Contrascere Date: Wed, 30 Dec 2015 16:45:20 -0500 Subject: [PATCH 735/890] Fix 'RemovedInDjango110Warning: SubfieldBase has been deprecated.' messages from Django 1.9. --- social/apps/django_app/default/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py index acdc6eae0..4a865eedc 100644 --- a/social/apps/django_app/default/fields.py +++ b/social/apps/django_app/default/fields.py @@ -11,7 +11,7 @@ from django.utils.encoding import smart_text -class JSONField(six.with_metaclass(models.SubfieldBase, models.TextField)): +class JSONField(models.TextField): """Simple JSON field that stores python structures as JSON strings on database. """ From d8d711946bbf6c4eddfb8d43c1cf37f1fc03e89c Mon Sep 17 00:00:00 2001 From: Nick Catalano Date: Wed, 30 Sep 2015 02:26:13 -0500 Subject: [PATCH 736/890] Add NGP VAN ActionID support --- README.rst | 2 + docs/backends/index.rst | 1 + docs/backends/ngpvan_actionid.rst | 36 +++++ docs/intro.rst | 2 + social/backends/ngpvan.py | 61 +++++++++ social/tests/backends/test_ngpvan.py | 193 +++++++++++++++++++++++++++ 6 files changed, 295 insertions(+) create mode 100644 docs/backends/ngpvan_actionid.rst create mode 100644 social/backends/ngpvan.py create mode 100644 social/tests/backends/test_ngpvan.py diff --git a/README.rst b/README.rst index 03df9cd74..3117b7d29 100644 --- a/README.rst +++ b/README.rst @@ -93,6 +93,7 @@ or current ones extended): * `Moves app`_ OAuth2 https://dev.moves-app.com/docs/authentication * `Mozilla Persona`_ * NaszaKlasa_ OAuth2 + * `NGPVAN ActionID`_ OpenId * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth @@ -267,6 +268,7 @@ check `django-social-auth LICENSE`_ for details: .. _Moves app: https://dev.moves-app.com/docs/ .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _NaszaKlasa: https://developers.nk.pl/ +.. _NGPVAN ActionID: http://developers.ngpvan.com/action-id .. _Odnoklassniki: http://www.odnoklassniki.ru .. _Pocket: http://getpocket.com .. _Podio: https://podio.com diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 799ca6238..c54ce049f 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -98,6 +98,7 @@ Social backends moves naszaklasa nationbuilder + ngpvan_actionid odnoklassnikiru openstreetmap orbi diff --git a/docs/backends/ngpvan_actionid.rst b/docs/backends/ngpvan_actionid.rst new file mode 100644 index 000000000..cc980a70d --- /dev/null +++ b/docs/backends/ngpvan_actionid.rst @@ -0,0 +1,36 @@ +NGP VAN ActionID +================ + +`NGP VAN`_'s ActionID_ service provides an OpenID 1.1 endpoint, which provides +first name, last name, email address, and phone number. + +ActionID doesn't require major settings beside being defined on +``AUTHENTICATION_BACKENDS`` + +.. code-block:: python + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.ngpvan.ActionIDOpenID', + ... + ) + + +If you want to be able to access the "phone" attribute offered by NGP VAN +within ``extra_data`` you can add the following to your settings: + +.. code-block:: python + + SOCIAL_AUTH_ACTIONID_OPENID_AX_EXTRA_DATA = [ + ('http://openid.net/schema/contact/phone/business', 'phone') + ] + + +NGP VAN offers the ability to have your domain whitelisted, which will disable +the "{domain} is requesting a link to your ActionID" warning when your app +attempts to login using an ActionID account. Contact +`NGP VAN Developer Support`_ for more information + +.. _NGP VAN: http://www.ngpvan.com/ +.. _ActionID: http://developers.ngpvan.com/action-id +.. _NGP VAN Developer Support: http://developers.ngpvan.com/support/contact diff --git a/docs/intro.rst b/docs/intro.rst index b138974dc..191d7eb44 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -61,6 +61,7 @@ or extend current one): * Mixcloud_ OAuth2 * `Mozilla Persona`_ * NaszaKlasa_ OAuth2 + * `NGPVAN ActionID`_ OpenId * Odnoklassniki_ OAuth2 and Application Auth * OpenId_ * Podio_ OAuth2 @@ -141,6 +142,7 @@ section. .. _Mixcloud: https://www.mixcloud.com .. _Mozilla Persona: http://www.mozilla.org/persona/ .. _NaszaKlasa: https://developers.nk.pl/ +.. _NGPVAN ActionID: http://developers.ngpvan.com/action-id .. _Odnoklassniki: http://www.odnoklassniki.ru .. _Podio: https://podio.com .. _Shopify: http://shopify.com diff --git a/social/backends/ngpvan.py b/social/backends/ngpvan.py new file mode 100644 index 000000000..42a34c81e --- /dev/null +++ b/social/backends/ngpvan.py @@ -0,0 +1,61 @@ +""" +NGP VAN's `ActionID` Provider + +http://developers.ngpvan.com/action-id +""" +from social.backends.open_id import OpenIdAuth +from openid.extensions import ax + + +class ActionIDOpenID(OpenIdAuth): + """ + NGP VAN's ActionID OpenID 1.1 authentication backend + """ + name = 'actionid-openid' + URL = 'https://accounts.ngpvan.com/Home/Xrds' + USERNAME_KEY = 'email' + + def get_ax_attributes(self): + """ + Return the AX attributes that ActionID responds with, as well as the + user data result that it must map to. + """ + return [ + ('http://openid.net/schema/contact/internet/email', 'email'), + ('http://openid.net/schema/contact/phone/business', 'phone'), + ('http://openid.net/schema/namePerson/first', 'first_name'), + ('http://openid.net/schema/namePerson/last', 'last_name'), + ('http://openid.net/schema/namePerson', 'fullname'), + ] + + def setup_request(self, params=None): + """ + Setup the OpenID request + + Because ActionID does not advertise the availiability of AX attributes + nor use standard attribute aliases, we need to setup the attributes + manually instead of rely on the parent OpenIdAuth.setup_request() + """ + request = self.openid_request(params) + + fetch_request = ax.FetchRequest() + fetch_request.add(ax.AttrInfo( + 'http://openid.net/schema/contact/internet/email', + alias='ngpvanemail', + required=True)) + + fetch_request.add(ax.AttrInfo( + 'http://openid.net/schema/contact/phone/business', + alias='ngpvanphone', + required=False)) + fetch_request.add(ax.AttrInfo( + 'http://openid.net/schema/namePerson/first', + alias='ngpvanfirstname', + required=False)) + fetch_request.add(ax.AttrInfo( + 'http://openid.net/schema/namePerson/last', + alias='ngpvanlastname', + required=False)) + request.addExtension(fetch_request) + + return request diff --git a/social/tests/backends/test_ngpvan.py b/social/tests/backends/test_ngpvan.py new file mode 100644 index 000000000..1189839a1 --- /dev/null +++ b/social/tests/backends/test_ngpvan.py @@ -0,0 +1,193 @@ +"""Tests for NGP VAN ActionID Backend""" +import datetime + +from httpretty import HTTPretty + +from social.p3 import urlencode +from social.tests.backends.open_id import OpenIdTest + + +JANRAIN_NONCE = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + + +class NGPVANActionIDOpenIDTest(OpenIdTest): + """Test the NGP VAN ActionID OpenID 1.1 Backend""" + backend_path = 'social.backends.ngpvan.ActionIDOpenID' + expected_username = 'testuser@user.local' + discovery_body = ' '.join( + [ + '', + '', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/extensions/sreg/1.1', + 'http://axschema.org/contact/email', + 'https://accounts.ngpvan.com/OpenId/Provider', + '', + '', + 'http://openid.net/signon/1.0', + 'http://openid.net/extensions/sreg/1.1', + 'http://axschema.org/contact/email', + 'https://accounts.ngpvan.com/OpenId/Provider', + '', + '', + '', + ]) + server_response = urlencode({ + 'openid.claimed_id': 'https://accounts.ngpvan.com/user/abcd123', + 'openid.identity': 'https://accounts.ngpvan.com/user/abcd123', + 'openid.sig': 'Midw8F/rCDwW7vMz3y+vK6rjz6s=', + 'openid.signed': 'claimed_id,identity,assoc_handle,op_endpoint,return_' + 'to,response_nonce,ns.alias3,alias3.mode,alias3.type.' + 'alias1,alias3.value.alias1,alias3.type.alias2,alias3' + '.value.alias2,alias3.type.alias3,alias3.value.alias3' + ',alias3.type.alias4,alias3.value.alias4,alias3.type.' + 'alias5,alias3.value.alias5,alias3.type.alias6,alias3' + '.value.alias6,alias3.type.alias7,alias3.value.alias7' + ',alias3.type.alias8,alias3.value.alias8,ns.sreg,sreg' + '.fullname', + 'openid.assoc_handle': '{635790678917902781}{GdSyFA==}{20}', + 'openid.op_endpoint': 'https://accounts.ngpvan.com/OpenId/Provider', + 'openid.return_to': 'http://myapp.com/complete/actionid-openid/', + 'openid.response_nonce': JANRAIN_NONCE + 'MMgBGEre', + 'openid.mode': 'id_res', + 'openid.ns': 'http://specs.openid.net/auth/2.0', + 'openid.ns.alias3': 'http://openid.net/srv/ax/1.0', + 'openid.alias3.mode': 'fetch_response', + 'openid.alias3.type.alias1': 'http://openid.net/schema/contact/phone/b' + 'usiness', + 'openid.alias3.value.alias1': '+12015555555', + 'openid.alias3.type.alias2': 'http://openid.net/schema/contact/interne' + 't/email', + 'openid.alias3.value.alias2': 'testuser@user.local', + 'openid.alias3.type.alias3': 'http://openid.net/schema/namePerson/firs' + 't', + 'openid.alias3.value.alias3': 'John', + 'openid.alias3.type.alias4': 'http://openid.net/schema/namePerson/las' + 't', + 'openid.alias3.value.alias4': 'Smith', + 'openid.alias3.type.alias5': 'http://axschema.org/namePerson/first', + 'openid.alias3.value.alias5': 'John', + 'openid.alias3.type.alias6': 'http://axschema.org/namePerson/last', + 'openid.alias3.value.alias6': 'Smith', + 'openid.alias3.type.alias7': 'http://axschema.org/namePerson', + 'openid.alias3.value.alias7': 'John Smith', + 'openid.alias3.type.alias8': 'http://openid.net/schema/namePerson', + 'openid.alias3.value.alias8': 'John Smith', + 'openid.ns.sreg': 'http://openid.net/extensions/sreg/1.1', + 'openid.sreg.fullname': 'John Smith', + }) + + def setUp(self): + """Setup the test""" + super(NGPVANActionIDOpenIDTest, self).setUp() + + # Mock out the NGP VAN endpoints + HTTPretty.register_uri( + HTTPretty.POST, + 'https://accounts.ngpvan.com/Home/Xrds', + status=200, + body=self.discovery_body) + HTTPretty.register_uri( + HTTPretty.GET, + 'https://accounts.ngpvan.com/user/abcd123', + status=200, + body=self.discovery_body) + HTTPretty.register_uri( + HTTPretty.GET, + 'https://accounts.ngpvan.com/OpenId/Provider', + status=200, + body=self.discovery_body) + + def test_login(self): + """Test the login flow using python-social-auth's built in test""" + self.do_login() + + def test_partial_pipeline(self): + """Test the partial flow using python-social-auth's built in test""" + self.do_partial_pipeline() + + def test_get_ax_attributes(self): + """Test that the AX attributes that NGP VAN responds with are present""" + records = self.backend.get_ax_attributes() + + self.assertEqual( + records, + [ + ('http://openid.net/schema/contact/internet/email', 'email'), + ('http://openid.net/schema/contact/phone/business', 'phone'), + ('http://openid.net/schema/namePerson/first', 'first_name'), + ('http://openid.net/schema/namePerson/last', 'last_name'), + ('http://openid.net/schema/namePerson', 'fullname'), + ] + ) + + def test_setup_request(self): + """Test the setup_request functionality in the NGP VAN backend""" + # We can grab the requested attributes by grabbing the HTML of the + # OpenID auth form and pulling out the hidden fields + _, inputs = self.get_form_data(self.backend.auth_html()) + + # Confirm that the only required attribute is email + self.assertEqual(inputs['openid.ax.required'], 'ngpvanemail') + + # Confirm that the 3 optional attributes are requested "if available" + self.assertIn('ngpvanphone', inputs['openid.ax.if_available']) + self.assertIn('ngpvanfirstname', inputs['openid.ax.if_available']) + self.assertIn('ngpvanlastname', inputs['openid.ax.if_available']) + + # Verify the individual attribute properties + self.assertEqual( + inputs['openid.ax.type.ngpvanemail'], + 'http://openid.net/schema/contact/internet/email') + self.assertEqual( + inputs['openid.ax.type.ngpvanfirstname'], + 'http://openid.net/schema/namePerson/first') + self.assertEqual( + inputs['openid.ax.type.ngpvanlastname'], + 'http://openid.net/schema/namePerson/last') + self.assertEqual( + inputs['openid.ax.type.ngpvanphone'], + 'http://openid.net/schema/contact/phone/business') + + def test_user_data(self): + """Ensure that the correct user data is being passed to create_user""" + self.strategy.set_settings({ + 'USER_FIELDS': [ + 'email', + 'first_name', + 'last_name', + 'username', + 'phone', + 'fullname' + ] + }) + user = self.do_start() + + self.assertEqual(user.username, u'testuser@user.local') + self.assertEqual(user.email, u'testuser@user.local') + self.assertEqual(user.extra_user_fields['phone'], u'+12015555555') + self.assertEqual(user.extra_user_fields['first_name'], u'John') + self.assertEqual(user.extra_user_fields['last_name'], u'Smith') + self.assertEqual(user.extra_user_fields['fullname'], u'John Smith') + + def test_extra_data_phone(self): + """Confirm that you can get a phone number via the relevant setting""" + self.strategy.set_settings({ + 'SOCIAL_AUTH_ACTIONID_OPENID_AX_EXTRA_DATA': [ + ('http://openid.net/schema/contact/phone/business', 'phone') + ] + }) + user = self.do_start() + self.assertEqual(user.social_user.extra_data['phone'], u'+12015555555') + + def test_association_uid(self): + """Test that the correct association uid is stored in the database""" + user = self.do_start() + self.assertEqual( + user.social_user.uid, + 'https://accounts.ngpvan.com/user/abcd123') From d77509b601547c1cebd831cd337d2bf9565eb312 Mon Sep 17 00:00:00 2001 From: Marc Hoersken Date: Mon, 4 Jan 2016 14:48:06 +0100 Subject: [PATCH 737/890] amazon backend: change authorization URL from http to https --- social/backends/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/amazon.py b/social/backends/amazon.py index 247574454..eb1d8ff79 100644 --- a/social/backends/amazon.py +++ b/social/backends/amazon.py @@ -10,7 +10,7 @@ class AmazonOAuth2(BaseOAuth2): name = 'amazon' ID_KEY = 'user_id' - AUTHORIZATION_URL = 'http://www.amazon.com/ap/oa' + AUTHORIZATION_URL = 'https://www.amazon.com/ap/oa' ACCESS_TOKEN_URL = 'https://api.amazon.com/auth/o2/token' DEFAULT_SCOPE = ['profile'] REDIRECT_STATE = False From faf039c8cb85cb1bfc0872f4ce4ee4a861f5b6cc Mon Sep 17 00:00:00 2001 From: Marc Hoersken Date: Mon, 4 Jan 2016 14:48:49 +0100 Subject: [PATCH 738/890] twilio backend: fix backend name in comment --- social/backends/twilio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/twilio.py b/social/backends/twilio.py index 96621dd7e..d63fe6c3a 100644 --- a/social/backends/twilio.py +++ b/social/backends/twilio.py @@ -1,5 +1,5 @@ """ -Amazon auth backend, docs at: +Twilio auth backend, docs at: http://psa.matiasaguirre.net/docs/backends/twilio.html """ from re import sub From 557341d5561c13530644131b433f3b7a395155fb Mon Sep 17 00:00:00 2001 From: Kevin Chang Date: Thu, 7 Jan 2016 15:47:28 -0800 Subject: [PATCH 739/890] Store access token in response if it does not exist `BaseOAuth2.do_auth` wasn't storing the access token to be passed to the pipeline. As a result, backends inheriting from `BaseOAuth2` wasn't storing the token because `OAuthAuth.extra_data` never had the token information passed to it. --- social/backends/oauth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index b6d7abbc0..b2210ec0b 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -392,6 +392,8 @@ def do_auth(self, access_token, *args, **kwargs): data = self.user_data(access_token, *args, **kwargs) response = kwargs.get('response') or {} response.update(data or {}) + if 'access_token' not in response: + response['access_token'] = access_token kwargs.update({'response': response, 'backend': self}) return self.strategy.authenticate(*args, **kwargs) From 53902cb854bb7f20d0692339606d2496fc8152af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 14 Jan 2016 15:56:15 -0300 Subject: [PATCH 740/890] PEP8 --- social/apps/django_app/urls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/social/apps/django_app/urls.py b/social/apps/django_app/urls.py index 1d72e5b14..fc85107fb 100644 --- a/social/apps/django_app/urls.py +++ b/social/apps/django_app/urls.py @@ -6,14 +6,12 @@ # Django < 1.4 from django.conf.urls.defaults import url - from social.utils import setting_name from social.apps.django_app import views extra = getattr(settings, setting_name('TRAILING_SLASH'), True) and '/' or '' - urlpatterns = [ # authentication / association url(r'^login/(?P[^/]+){0}$'.format(extra), views.auth, From 674e4f193b0084d3f926cae0aac27e06b7750bf2 Mon Sep 17 00:00:00 2001 From: "Alain St. Pierre" Date: Thu, 14 Jan 2016 16:55:47 -0800 Subject: [PATCH 741/890] added support for ArcGIS OAuth2 --- docs/backends/arcgis.rst | 26 ++++++++++++++++++++++++++ docs/thanks.rst | 2 ++ social/backends/arcgis.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 docs/backends/arcgis.rst create mode 100644 social/backends/arcgis.py diff --git a/docs/backends/arcgis.rst b/docs/backends/arcgis.rst new file mode 100644 index 000000000..6c402cd45 --- /dev/null +++ b/docs/backends/arcgis.rst @@ -0,0 +1,26 @@ +ArcGIS +===== + +ArcGIS uses OAuth2 for authentication. + +- Register a new application at `ArcGIS Developer Center`_. + + +OAuth2 +------------ + +1. Add the OAuth2 backend to your settings page:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.arcgis.ArcGISOAuth2', + ... + ) + +2. Fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_ARCGIS_KEY = '' + SOCIAL_AUTH_ARCGIS_SECRET = '' + + +.. _ArcGIS Developer Center: https://developers.arcgis.com/ diff --git a/docs/thanks.rst b/docs/thanks.rst index 8dd42c9f3..ca4a40e68 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -109,6 +109,7 @@ let me know and I'll update the list): * bluszcz_ * vbsteven_ * sbassi_ + * aspcanada_ .. _python-social-auth: https://github.com/omab/python-social-auth @@ -213,3 +214,4 @@ let me know and I'll update the list): .. _bluszcz: https://github.com/bluszcz .. _vbsteven: https://github.com/vbsteven .. _sbassi: https://github.com/sbassi +.. _aspcanada: https://github.com/aspcanada diff --git a/social/backends/arcgis.py b/social/backends/arcgis.py new file mode 100644 index 000000000..7f54b3c48 --- /dev/null +++ b/social/backends/arcgis.py @@ -0,0 +1,35 @@ +""" +ArcGIS OAuth2 backend +""" +from social.backends.oauth import BaseOAuth2 + + +class ArcGISOAuth2(BaseOAuth2): + name = 'arcgis' + ID_KEY = 'username' + AUTHORIZATION_URL = 'https://www.arcgis.com/sharing/rest/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://www.arcgis.com/sharing/rest/oauth2/token' + ACCESS_TOKEN_METHOD = 'POST' + EXTRA_DATA = [ + ('expires_in', 'expires_in') + ] + + def get_user_details(self, response): + """Return user details from ArcGIS account""" + return {'username': response.get('username'), + 'email': response.get('email'), + 'fullname': response.get('fullName'), + 'first_name': response.get('firstName'), + 'last_name': response.get('lastName')} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + data = self.get_json( + 'https://www.arcgis.com/sharing/rest/community/self', + params={ + 'token': access_token, + 'f': 'json' + } + ) + + return data From 3ef0fea1e8a492e12a2b06d82b345bdf377e1787 Mon Sep 17 00:00:00 2001 From: "Alain St. Pierre" Date: Thu, 14 Jan 2016 17:04:33 -0800 Subject: [PATCH 742/890] updated README for ArcGIS addition --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 03df9cd74..c2d924db3 100644 --- a/README.rst +++ b/README.rst @@ -56,6 +56,7 @@ or current ones extended): * Angel_ OAuth2 * AOL_ OpenId http://www.aol.com/ * Appsfuel_ OAuth2 + * ArcGIS_ OAuth2 * Behance_ OAuth2 * BelgiumEIDOpenId_ OpenId https://www.e-contract.be/ * Bitbucket_ OAuth1 @@ -237,6 +238,7 @@ check `django-social-auth LICENSE`_ for details: .. _myOpenID: https://www.myopenid.com/ .. _Angel: https://angel.co .. _Appsfuel: http://docs.appsfuel.com +.. _ArcGIS: http://www.arcgis.com/ .. _Behance: https://www.behance.net .. _Bitbucket: https://bitbucket.org .. _Box: https://www.box.com From ca3a71f43b4ac587f806acf77d6882f6a4d83dff Mon Sep 17 00:00:00 2001 From: Omar Khan Date: Fri, 15 Jan 2016 11:35:13 +0700 Subject: [PATCH 743/890] SAML: raise AuthMissingParameter if idp param missing The SAML login handler fails with a KeyError if the idp query param is not given, which leads to a 500 response. Raise AuthMissingParameter instead. --- social/backends/saml.py | 7 +++++-- social/tests/backends/test_saml.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/social/backends/saml.py b/social/backends/saml.py index 206b26c6f..a249c0b40 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -11,7 +11,7 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from social.backends.base import BaseAuth -from social.exceptions import AuthFailed +from social.exceptions import AuthFailed, AuthMissingParameter # Helpful constants: OID_COMMON_NAME = "urn:oid:2.5.4.3" @@ -256,7 +256,10 @@ def _create_saml_auth(self, idp): def auth_url(self): """Get the URL to which we must redirect in order to authenticate the user""" - idp_name = self.strategy.request_data()['idp'] + try: + idp_name = self.strategy.request_data()['idp'] + except KeyError: + raise AuthMissingParameter(self, 'idp') auth = self._create_saml_auth(idp=self.get_idp(idp_name)) # Below, return_to sets the RelayState, which can contain # arbitrary data. We use it to store the specific SAML IdP diff --git a/social/tests/backends/test_saml.py b/social/tests/backends/test_saml.py index 2cd552087..f9fd4d41b 100644 --- a/social/tests/backends/test_saml.py +++ b/social/tests/backends/test_saml.py @@ -15,6 +15,7 @@ pass from social.tests.backends.base import BaseBackendTest +from social.exceptions import AuthMissingParameter from social.p3 import urlparse, urlunparse, urlencode, parse_qs DATA_DIR = path.join(path.dirname(__file__), 'data') @@ -64,8 +65,6 @@ def install_http_intercepts(self, start_url, return_url): body='foobar') def do_start(self): - # pretend we've started with a URL like /login/saml/?idp=testshib: - self.strategy.set_request_data({'idp': 'testshib'}, self.backend) start_url = self.backend.start().url # Modify the start URL to make the SAML request consistent # from test to test: @@ -91,8 +90,15 @@ def test_metadata_generation(self): def test_login(self): """Test that we can authenticate with a SAML IdP (TestShib)""" + # pretend we've started with a URL like /login/saml/?idp=testshib: + self.strategy.set_request_data({'idp': 'testshib'}, self.backend) self.do_login() + def test_login_no_idp(self): + """Logging in without an idp param should raise AuthMissingParameter""" + with self.assertRaises(AuthMissingParameter): + self.do_start() + def modify_start_url(self, start_url): """ Given a SAML redirect URL, parse it and change the ID to From 3e1329f5c78c21bb5f59425a0508859de1ccdeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 25 Jan 2016 12:20:27 -0300 Subject: [PATCH 744/890] v0.2.14 --- Changelog | 374 +++++++++++++++++++++++++++++++++++++++++---- social/__init__.py | 2 +- 2 files changed, 348 insertions(+), 28 deletions(-) diff --git a/Changelog b/Changelog index 76fb91905..abe8ab835 100644 --- a/Changelog +++ b/Changelog @@ -1,9 +1,329 @@ +2016-01-14 HEAD (unreleased) +============================ + + * 2016-01-14 Matías Aguirre + PEP8 + + * 2015-12-23 Matías Aguirre + Document run_tox script dependencies + + * 2015-12-23 Matías Aguirre + PEP8 and doc styles applied + + * 2015-12-16 Yuri Prezument + Fix Django 1.10 deprecation warnings + + * 2015-12-08 Matías Aguirre + Typos and PEP8 + + * 2015-12-05 Romulo Tavares + Changed instagram backend to new authorization routes + + * 2015-12-01 James Keys + Update settings.rst + + * 2015-11-23 Matías Aguirre + Add instagram authentication backend to docs + + * 2015-11-17 SeokJun Hong + fix Travis-CI failed + + * 2015-11-16 SeokJun Hong + fix a typing error in naver.rst + + * 2015-11-16 SeokJun Hong + Add naver backends + + * 2015-11-11 Matthew Jones + Formatter fixes for SAML to support Py2.6 + + * 2015-10-20 Martín Prunell + Fix typo + + * 2015-10-17 lneoe + use `self.setting('USE_OPENID_AS_USERNAME', False)` this will fast + + * 2015-10-14 lneoe + use `openid` as username + + * 2015-10-16 Kevin Harvey + Fixes a few grammar issues in the docs + + * 2015-10-12 Peter Schmidt + Fix a few typos in backends + + * 2015-10-07 Sergey Trofimov + Update odnoklassniki.py + + * 2015-10-07 Sergey Trofimov + Update vk.py + + * 2015-10-05 Matías Aguirre + Switch import order + + * 2015-10-05 Matías Aguirre + Link justgiving doc + + * 2015-09-16 Matías Aguirre + Fix missing slash + + * 2015-09-16 Matías Aguirre + Fix kwarg + + * 2015-09-16 Matías Aguirre + Fix typo + + * 2015-09-16 Matías Aguirre + Add signed request signature for Instagram + + * 2015-10-05 Matías Aguirre + Codestyle and missing docs + + * 2015-10-05 Matías Aguirre + Fix syntax error + + * 2015-10-05 Matías Aguirre + Remove python3.5 (openid breaks), silence pypy warings + + * 2015-10-05 Matías Aguirre + Remove duplicated pypy entry, add python 3.5 + + * 2015-10-05 Matías Aguirre + Pypy test requirements for travisci + + * 2015-10-05 Matías Aguirre + Fix pypy support, no python-saml for it, silence python2.6 warnings + + * 2015-10-04 Matías Aguirre + Fix travis conf + + * 2015-10-04 Matías Aguirre + Syntax typo + + * 2015-10-04 Matías Aguirre + Swtich travisci from legacy to container infrastructure + + * 2015-10-02 Maarten van Schaik + Store all tokens on refresh + + * 2015-09-29 Maarten van Schaik + Save extra_data on login + + * 2015-09-28 Luke Briner + Update URLs to match new site and remove OAuth comment. + +2015-09-25 v0.2.13 +================== + + * 2015-09-25 Matías Aguirre + v0.2.13 + + * 2015-09-23 James Maddox + added Python 3 support to FacebookAppOAuth2's load_signed_request method + + * 2015-09-15 alrusdi + VK API workflow fix if error happens on vk-side + + * 2015-09-07 Bulgantamir Gankhuyag + added AuthUnreachableProvider exception to documentation + + * 2015-09-02 Michael Willmott + Added justgiving.com OAuth2 backend + + * 2015-09-01 Julian Bez + Add REDIRECT_STATE = False + + * 2015-08-31 Matías Aguirre + Remove template tag + + * 2015-08-31 Matías Aguirre + Fix Google+ auth complete, update examples with updated SDK usage. Refs + #316 + + * 2015-08-24 Andy + Fix typo in pipeline doc + + * 2015-08-20 Michał Zagdan + Update facebook.rst + + * 2015-08-19 Jerzy Spendel + One more coma in use_cases.rst + + * 2015-08-18 Jerzy Spendel + Coma at the end of every tuple and dict + + * 2015-08-18 Jerzy Spendel + Tuple in pipeline's documentation should be ended with coma + + * 2015-08-15 Chun-Jung Lee + Support Pyramid Authentication Policies + + * 2015-08-14 Ajoy Oommen + Fix another mistake in use_cases.rst + + * 2015-08-14 Ajoy Oommen + Fix typo in use_cases.rst + + * 2015-08-14 Matías Aguirre + Small cosmetic changes and link from main index. Refs #700 + + * 2015-08-14 Matías Aguirre + Small cosmetic changes and link from main index. Refs #700 + + * 2015-08-10 Henoc Díaz + References to UberOAuth2 backend docs added in intro.rst and + backends/index.rst + + * 2015-08-10 Henoc Díaz + Add support for Uber OAuth2 - Uber API v1 - Backend UberOAuth2 added - + Tests for UberOAuth2 backend added - Docs for backend added + + * 2015-08-07 Matías Aguirre + Fix flask remember-me functionality + + * 2015-08-07 Matías Aguirre + Retrieve remember value from session + + * 2015-08-05 khamaileon + Fix #703 + + * 2015-07-31 Chris Curvey + formatting cleanups + + * 2015-07-31 Chris Curvey + more formatting + + * 2015-07-31 Chris Curvey + formatting test + + * 2015-07-31 Chris Curvey + first revision + + * 2015-07-29 Troy Grosfield + Adding abstract UserSocialAuth model as well as a model manager class. + + * 2015-07-29 Troy Grosfield + Adding a model manager for the django app. + + * 2015-07-25 James Little + Update main.py + + * 2015-07-22 Matías Aguirre + Link docs and PEP8 + + * 2015-07-21 Can Kaya + removed @app.teardown_request since it is called before + @app.teardown_appcontext and removes current session. With + @app.teardown_request session is removed just before session.commit and + logged in user can not be saved on db. + + * 2015-07-21 Georgy Cheshkov + Remove debug printing from BaseOAuth2 backend + + * 2015-07-19 João Miguel Neves + support for goclio.eu service + + * 2015-07-16 Jordan Reiter + text -> content solves "is not JSON serializable" + + * 2015-07-16 Frankie Robertson + Close #622 by explicitly setting email length (compatibility with Django + 1.7) + + * 2015-07-16 Lee Jaeyoung + Fix test failed: get right user_id from response + + * 2015-07-16 Lee Jaeyoung + Add orbi backend + + * 2015-07-15 Matías Aguirre + Meetup PEP8 and link docs + + * 2015-07-14 Jesse Pollak + fix clef backend by access ID in correct way + + * 2015-07-13 bluszcz + Clean up + + * 2015-07-13 bluszcz + Fixed typo + + * 2015-07-13 bluszcz + Added Meetup.com OAuth2 backend + + * 2015-07-10 paxapy + echosign OAuth2 backend + +2015-07-10 v0.2.12 +================== + + * 2015-07-10 Matías Aguirre + v0.2.12 + + * 2015-07-10 Julian Bez + Use full + + * 2015-07-10 Julian Bez + Use key + + * 2015-07-10 Julian Bez + Fix 'QueryDict' object has no attribute 'dicts' + + * 2015-07-10 Maarten van Schaik + Fix redirect_uri issue with tornado reversed url + + * 2015-07-09 Matías Aguirre + Fix doc title in withins file + + * 2015-07-09 Matías Aguirre + PEP8 and code simplification + + * 2015-07-09 eshellman + Update settings.rst + + * 2015-07-09 Matías Aguirre + Backward compatibility option. Refs #652 + + * 2015-07-09 Matías Aguirre + Add comment refering to ticket. Refs #671, #672 + + * 2015-07-09 Maksim Sokolskiy + fix(pipeline): fix user_detail pipeline issue + + * 2015-07-03 Maarten van Schaik + Fix decoding of request args + + * 2015-07-03 Maarten van Schaik + Fix cookie handling for tornado + + * 2015-06-28 Igor Serko + added support for Github Enterprise + + * 2015-06-25 Tomas + Withings Backend + + * 2015-06-24 Braden MacDonald + Use official python-saml 2.1.3 release, remove setting that is no longer + included + + * 2015-06-24 Braden MacDonald + Fix wrong placement of changelog commits in 76a27b2 + 2015-06-24 v0.2.11 ================== * 2015-06-24 Matías Aguirre v0.2.11 + * 2015-06-21 Mark Adams + Added an OAuth2 backend for Bitbucket + + * 2015-06-21 Mark Adams + Updated Bitbucket backend to use the UUID as the ID_KEY + + * 2015-06-21 Mark Adams + Updated Bitbucket backend to use newer 2.0 APIs instead of 1.0 + * 2015-06-19 Matías Aguirre Link docs @@ -13,30 +333,14 @@ * 2015-06-18 Braden MacDonald Added documentation + * 2015-06-17 Maarten van Schaik + Python3 fixes for Tornado + * 2015-06-17 Matías Aguirre PEP8 - * 2015-05-21 Braden MacDonald - Minor cleanups - - * 2015-05-20 Braden MacDonald - Minor consistency fix - - * 2015-05-20 Braden MacDonald - Add an integration point for extra security layers like - eduPersonEntitlement - - * 2015-05-20 Braden MacDonald - Make IdP name format slightly more flexible - - * 2015-05-11 Braden MacDonald - Add python-saml requirement (temporary commit) - - * 2015-05-08 Braden MacDonald - Tests for SAML backend - - * 2015-04-30 Braden MacDonald - SAML2 backend using OneLogin's python-saml + * 2015-06-01 Aurélien Bompard + Keep the egg-info directory in the sdist 2015-05-29 v0.2.10 ================== @@ -92,6 +396,19 @@ * 2015-05-11 sushantgawali Added provider for Microsoft Azure Active Directory OAuth2 + * 2015-05-21 Braden MacDonald + Minor cleanups + + * 2015-05-20 Braden MacDonald + Minor consistency fix + + * 2015-05-20 Braden MacDonald + Add an integration point for extra security layers like + eduPersonEntitlement + + * 2015-05-20 Braden MacDonald + Make IdP name format slightly more flexible + * 2015-05-20 Marek Jalovec Fixes "ImportError: No module named packages.urllib3.poolmanager" error (fixes #617) @@ -105,6 +422,15 @@ * 2015-05-16 Andrew Starr-Bochicchio Add a DigitalOcean backend. + * 2015-05-11 Braden MacDonald + Add python-saml requirement (temporary commit) + + * 2015-05-08 Braden MacDonald + Tests for SAML backend + + * 2015-04-30 Braden MacDonald + SAML2 backend using OneLogin's python-saml + * 2015-05-08 Matías Aguirre Ensure that all the requirements are installed @@ -858,9 +1184,6 @@ * 2014-09-11 Matías Aguirre Flag dev version -2014-09-11 v0.2.1 -================= - * 2014-09-11 Matías Aguirre v0.2.1 @@ -873,9 +1196,6 @@ * 2014-09-11 Matías Aguirre Flag dev version -2014-09-11 v0.2.0 -================= - * 2014-09-11 Matías Aguirre v0.2.0 diff --git a/social/__init__.py b/social/__init__.py index 6294763db..e45dc94c7 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 13) +version = (0, 2, 14) extra = '' __version__ = '.'.join(map(str, version)) + extra From b07708efe7d19b75009771aa97ddf821e59ec08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 26 Jan 2016 12:53:28 -0300 Subject: [PATCH 745/890] Update changelog with output from github_changelog_generator --- CHANGELOG.md | 923 +++++++++++ Changelog | 4213 -------------------------------------------------- MANIFEST.in | 2 +- 3 files changed, 924 insertions(+), 4214 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 Changelog diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..85b977313 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,923 @@ +# Change Log + +## [v0.2.14](https://github.com/omab/python-social-auth/tree/v0.2.14) (2016-01-25) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.13...v0.2.14) + +**Closed issues:** + +- Error "imported before its application was loaded" [\#809](https://github.com/omab/python-social-auth/issues/809) +- Django 1.9.0 Deprecation Warning [\#804](https://github.com/omab/python-social-auth/issues/804) +- Migration error on update 0.2.6 -\> 0.2.7 [\#761](https://github.com/omab/python-social-auth/issues/761) +- Backends: user\_data vs extra\_data? [\#759](https://github.com/omab/python-social-auth/issues/759) +- example/django\_example twitter error [\#742](https://github.com/omab/python-social-auth/issues/742) +- Object of type map has no length [\#633](https://github.com/omab/python-social-auth/issues/633) + +**Merged pull requests:** + +- Fix Django 1.10 deprecation warnings [\#806](https://github.com/omab/python-social-auth/pull/806) ([yprez](https://github.com/yprez)) +- Changed instagram backend to new authorization routes [\#797](https://github.com/omab/python-social-auth/pull/797) ([clybob](https://github.com/clybob)) +- Update settings.rst [\#793](https://github.com/omab/python-social-auth/pull/793) ([skolsuper](https://github.com/skolsuper)) +- Add naver.com OAuth2 backend [\#789](https://github.com/omab/python-social-auth/pull/789) ([se0kjun](https://github.com/se0kjun)) +- Formatter fixes for SAML to support Py2.6 [\#783](https://github.com/omab/python-social-auth/pull/783) ([matburt](https://github.com/matburt)) +- Fix typo [\#768](https://github.com/omab/python-social-auth/pull/768) ([mprunell](https://github.com/mprunell)) +- Fixes a few grammar issues in the docs [\#764](https://github.com/omab/python-social-auth/pull/764) ([kevinharvey](https://github.com/kevinharvey)) +- use qq openid as username [\#763](https://github.com/omab/python-social-auth/pull/763) ([lneoe](https://github.com/lneoe)) +- Fix a few typos in backends [\#760](https://github.com/omab/python-social-auth/pull/760) ([pzrq](https://github.com/pzrq)) +- Fix vk backend [\#757](https://github.com/omab/python-social-auth/pull/757) ([truetug](https://github.com/truetug)) +- Fix odnoklassniki backend [\#756](https://github.com/omab/python-social-auth/pull/756) ([truetug](https://github.com/truetug)) +- Store all tokens when tokens are refreshed [\#753](https://github.com/omab/python-social-auth/pull/753) ([mvschaik](https://github.com/mvschaik)) +- Python 3 support for facebook-app backend [\#749](https://github.com/omab/python-social-auth/pull/749) ([jhmaddox](https://github.com/jhmaddox)) +- Save extra\_data on login [\#748](https://github.com/omab/python-social-auth/pull/748) ([mvschaik](https://github.com/mvschaik)) +- Update URLs to match new site and remove OAuth comment. [\#744](https://github.com/omab/python-social-auth/pull/744) ([lukos](https://github.com/lukos)) +- added AuthUnreachableProvider exception to documentation [\#729](https://github.com/omab/python-social-auth/pull/729) ([Qlio](https://github.com/Qlio)) +- Add REDIRECT\_STATE = False [\#725](https://github.com/omab/python-social-auth/pull/725) ([webjunkie](https://github.com/webjunkie)) +- Tuple in pipeline's documentation should be ended with coma [\#712](https://github.com/omab/python-social-auth/pull/712) ([JerzySpendel](https://github.com/JerzySpendel)) +- Fix redirect\_uri issue with tornado reversed url [\#674](https://github.com/omab/python-social-auth/pull/674) ([mvschaik](https://github.com/mvschaik)) + +## [v0.2.13](https://github.com/omab/python-social-auth/tree/v0.2.13) (2015-09-25) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.12...v0.2.13) + +**Closed issues:** + +- Signup by OAuth access\_token example question [\#737](https://github.com/omab/python-social-auth/issues/737) +- Connecting to a "django oAuth toolkit" based oAuth provider [\#727](https://github.com/omab/python-social-auth/issues/727) +- Exception Value: 'module' object has no attribute 'FacebookOauth2' [\#722](https://github.com/omab/python-social-auth/issues/722) +- Google OAuth2 - stopped working, now getting JSONDecodeError for token response [\#718](https://github.com/omab/python-social-auth/issues/718) +- Is there a conflict with django-debug-toolbar? [\#714](https://github.com/omab/python-social-auth/issues/714) +- FORM\_HTML and Legacy Auth [\#705](https://github.com/omab/python-social-auth/issues/705) +- Authentication process canceled with Spotify auth \(invalid\_client\) [\#703](https://github.com/omab/python-social-auth/issues/703) +- \[Question\] How to tell if a user was created or existing [\#701](https://github.com/omab/python-social-auth/issues/701) +- Make an abstract verstion of django's UserSocialAuth's model so it can be extended [\#698](https://github.com/omab/python-social-auth/issues/698) +- Problem porting from django-social-auth to python-social-auth [\#682](https://github.com/omab/python-social-auth/issues/682) +- django\_app/default: Migration 0003\_alter\_email\_max\_length wrong for Django 1.7 [\#622](https://github.com/omab/python-social-auth/issues/622) + +**Merged pull requests:** + +- VK API workflow fix if error happens on vk-side [\#736](https://github.com/omab/python-social-auth/pull/736) ([alrusdi](https://github.com/alrusdi)) +- Added justgiving.com OAuth2 backend [\#728](https://github.com/omab/python-social-auth/pull/728) ([mwillmott](https://github.com/mwillmott)) +- Fix typo in pipeline doc [\#720](https://github.com/omab/python-social-auth/pull/720) ([Andygmb](https://github.com/Andygmb)) +- Update facebook.rst [\#717](https://github.com/omab/python-social-auth/pull/717) ([zergu](https://github.com/zergu)) +- Support Pyramid Authentication Policies [\#710](https://github.com/omab/python-social-auth/pull/710) ([cjltsod](https://github.com/cjltsod)) +- Fix typo [\#709](https://github.com/omab/python-social-auth/pull/709) ([ajoyoommen](https://github.com/ajoyoommen)) +- Fix 'QueryDict' object has no attribute 'dicts' [\#707](https://github.com/omab/python-social-auth/pull/707) ([webjunkie](https://github.com/webjunkie)) +- Add support for Uber OAuth2 - Uber API v1 [\#706](https://github.com/omab/python-social-auth/pull/706) ([henocdz](https://github.com/henocdz)) +- Fix \#703 invalid\_client error with Spotify backend [\#704](https://github.com/omab/python-social-auth/pull/704) ([khamaileon](https://github.com/khamaileon)) +- additional "how it fits together" documentation [\#700](https://github.com/omab/python-social-auth/pull/700) ([ccurvey](https://github.com/ccurvey)) +- Make an abstract verstion of django's UserSocialAuth's model so it can be extended \(fixes \#698\) [\#699](https://github.com/omab/python-social-auth/pull/699) ([troygrosfield](https://github.com/troygrosfield)) +- flask\_me\_example fix [\#696](https://github.com/omab/python-social-auth/pull/696) ([jameslittle](https://github.com/jameslittle)) +- removed @app.teardown\_request since it is called before @app.teardown… [\#690](https://github.com/omab/python-social-auth/pull/690) ([asimcan](https://github.com/asimcan)) +- Remove debug printing from BaseOAuth2 backend [\#689](https://github.com/omab/python-social-auth/pull/689) ([gcheshkov](https://github.com/gcheshkov)) +- support for goclio.eu service [\#686](https://github.com/omab/python-social-auth/pull/686) ([jneves](https://github.com/jneves)) +- text -\> content solves "is not JSON serializable" [\#685](https://github.com/omab/python-social-auth/pull/685) ([JordanReiter](https://github.com/JordanReiter)) +- Close \#622 by explicitly setting email length \(compatibility with Django 1.7\) [\#684](https://github.com/omab/python-social-auth/pull/684) ([frankier](https://github.com/frankier)) +- Add orbi backend [\#683](https://github.com/omab/python-social-auth/pull/683) ([jeyraof](https://github.com/jeyraof)) +- Fix Clef backend [\#681](https://github.com/omab/python-social-auth/pull/681) ([jessepollak](https://github.com/jessepollak)) +- Meetup.com OAuth2 provider [\#678](https://github.com/omab/python-social-auth/pull/678) ([bluszcz](https://github.com/bluszcz)) +- echosign OAuth2 backend [\#676](https://github.com/omab/python-social-auth/pull/676) ([paxapy](https://github.com/paxapy)) + +## [v0.2.12](https://github.com/omab/python-social-auth/tree/v0.2.12) (2015-07-10) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.11...v0.2.12) + +**Closed issues:** + +- Pipeline `user\_details` not changing empty and protected user fields [\#671](https://github.com/omab/python-social-auth/issues/671) +- Instagram: Missing needed parameter state [\#643](https://github.com/omab/python-social-auth/issues/643) +- Could not find required distribution python-social-auth [\#638](https://github.com/omab/python-social-auth/issues/638) +- Installing python-social-auth as a dependecie for mailman3 with buildout fails [\#623](https://github.com/omab/python-social-auth/issues/623) + +**Merged pull requests:** + +- Improve docs on SOCIAL\_AUTH\_NEW\_USER\_REDIRECT\_URL [\#673](https://github.com/omab/python-social-auth/pull/673) ([eshellman](https://github.com/eshellman)) +- PR fix `user\_details` pipeline issue [\#672](https://github.com/omab/python-social-auth/pull/672) ([maxsocl](https://github.com/maxsocl)) +- Fix cookie handling for tornado [\#667](https://github.com/omab/python-social-auth/pull/667) ([mvschaik](https://github.com/mvschaik)) +- added support for Github Enterprise [\#662](https://github.com/omab/python-social-auth/pull/662) ([iserko](https://github.com/iserko)) +- Withings Backend [\#658](https://github.com/omab/python-social-auth/pull/658) ([tomasgarzon](https://github.com/tomasgarzon)) +- Use official python-saml 2.1.3 release, remove now-unsupported setting [\#657](https://github.com/omab/python-social-auth/pull/657) ([bradenmacdonald](https://github.com/bradenmacdonald)) +- Fix wrong placement of changelog commits in 76a27b2 [\#656](https://github.com/omab/python-social-auth/pull/656) ([bradenmacdonald](https://github.com/bradenmacdonald)) +- Python3 fixes for Tornado [\#649](https://github.com/omab/python-social-auth/pull/649) ([mvschaik](https://github.com/mvschaik)) +- Keep the egg-info directory in the sdist [\#635](https://github.com/omab/python-social-auth/pull/635) ([abompard](https://github.com/abompard)) + +## [v0.2.11](https://github.com/omab/python-social-auth/tree/v0.2.11) (2015-06-24) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.10...v0.2.11) + +**Merged pull requests:** + +- Added an OAuth2 backend for Bitbucket [\#653](https://github.com/omab/python-social-auth/pull/653) ([mark-adams](https://github.com/mark-adams)) +- Updated Bitbucket backends to use newer v2.0 APIs [\#652](https://github.com/omab/python-social-auth/pull/652) ([mark-adams](https://github.com/mark-adams)) +- SAML support [\#616](https://github.com/omab/python-social-auth/pull/616) ([bradenmacdonald](https://github.com/bradenmacdonald)) + +## [v0.2.10](https://github.com/omab/python-social-auth/tree/v0.2.10) (2015-05-30) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.9...v0.2.10) + +**Closed issues:** + +- "UserSocialAuth.user" must be a "MyUser" instance [\#631](https://github.com/omab/python-social-auth/issues/631) +- ImportError: No module named packages.urllib3.poolmanager [\#617](https://github.com/omab/python-social-auth/issues/617) +- AuthStateMissing: Session value state missing on web.py example integration [\#611](https://github.com/omab/python-social-auth/issues/611) +- return pipeline data when doing oauth association [\#610](https://github.com/omab/python-social-auth/issues/610) +- Reverse with trailing slash in django urls is broken since 0.2.4 to 0.2.7 [\#609](https://github.com/omab/python-social-auth/issues/609) + +**Merged pull requests:** + +- Resubmitting pull request to add Azure Active Directory support [\#632](https://github.com/omab/python-social-auth/pull/632) ([vinhub](https://github.com/vinhub)) +- Fixes missing packages.urllib3.poolmanager \(fixes \#617\) [\#626](https://github.com/omab/python-social-auth/pull/626) ([marekjalovec](https://github.com/marekjalovec)) +- fix Fitbit OAuth 1 authorization URL [\#625](https://github.com/omab/python-social-auth/pull/625) ([blurrcat](https://github.com/blurrcat)) +- add weixin backends [\#621](https://github.com/omab/python-social-auth/pull/621) ([duoduo369](https://github.com/duoduo369)) +- Add a DigitalOcean backend. [\#619](https://github.com/omab/python-social-auth/pull/619) ([andrewsomething](https://github.com/andrewsomething)) + +## [v0.2.9](https://github.com/omab/python-social-auth/tree/v0.2.9) (2015-05-07) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.8...v0.2.9) + +## [v0.2.8](https://github.com/omab/python-social-auth/tree/v0.2.8) (2015-05-07) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.7...v0.2.8) + +**Closed issues:** + +- Can't get a Google OAuth2 refresh\_token [\#607](https://github.com/omab/python-social-auth/issues/607) +- Get the current logged user in the template [\#605](https://github.com/omab/python-social-auth/issues/605) +- Two diferent user profiles [\#604](https://github.com/omab/python-social-auth/issues/604) +- Login with Amazon TLS requests [\#603](https://github.com/omab/python-social-auth/issues/603) +- Release apps.py for apps [\#601](https://github.com/omab/python-social-auth/issues/601) +- migrations [\#600](https://github.com/omab/python-social-auth/issues/600) +- Authentication failed: Can't connect to HTTPS URL because the SSL module is not available. [\#598](https://github.com/omab/python-social-auth/issues/598) +- ConnectionError at /complete/steam You have not defined a default connection [\#597](https://github.com/omab/python-social-auth/issues/597) +- uncompleted extra\_data for access\_token, code, and expires in Google+ [\#596](https://github.com/omab/python-social-auth/issues/596) +- Token error: Missing unauthorized token [\#589](https://github.com/omab/python-social-auth/issues/589) +- Email validation needs an email parameter \(docs\) [\#577](https://github.com/omab/python-social-auth/issues/577) +- Login pipeline trying to create new user when user exists [\#562](https://github.com/omab/python-social-auth/issues/562) + +**Merged pull requests:** + +- Just add Moves App to the list of providers on README [\#606](https://github.com/omab/python-social-auth/pull/606) ([avibrazil](https://github.com/avibrazil)) +- ChangeTip Backend [\#599](https://github.com/omab/python-social-auth/pull/599) ([gorillamania](https://github.com/gorillamania)) + +## [v0.2.7](https://github.com/omab/python-social-auth/tree/v0.2.7) (2015-04-19) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.6...v0.2.7) + +**Closed issues:** + +- CLEAN\_USERNAME\_REGEX error [\#594](https://github.com/omab/python-social-auth/issues/594) +- JSONDecodeError at /complete/facebook [\#592](https://github.com/omab/python-social-auth/issues/592) + +**Merged pull requests:** + +- Fix the final\_username may be empty and will skip the loop. [\#595](https://github.com/omab/python-social-auth/pull/595) ([littlezz](https://github.com/littlezz)) +- Alter email max length for Django app [\#593](https://github.com/omab/python-social-auth/pull/593) ([JonesChi](https://github.com/JonesChi)) +- Append trailing slash in Django [\#591](https://github.com/omab/python-social-auth/pull/591) ([chripede](https://github.com/chripede)) + +## [v0.2.6](https://github.com/omab/python-social-auth/tree/v0.2.6) (2015-04-14) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.5...v0.2.6) + +**Closed issues:** + +- pypi package version 0.2.5 is missing requirements.txt from tests [\#590](https://github.com/omab/python-social-auth/issues/590) +- TypeError: object of type 'map' has no len\(\) [\#588](https://github.com/omab/python-social-auth/issues/588) +- please support weixin auth [\#481](https://github.com/omab/python-social-auth/issues/481) +- How to take the user's address on facebook? This has already been implemented? [\#470](https://github.com/omab/python-social-auth/issues/470) +- django social auth get wrong access\_token from google oauth2 [\#467](https://github.com/omab/python-social-auth/issues/467) +- Reddit OAuth2 401 Client Error Unauthorized [\#440](https://github.com/omab/python-social-auth/issues/440) +- twitter login: 401 Client Error: Authorization Required [\#400](https://github.com/omab/python-social-auth/issues/400) +- remove incomplete partial pipeline data from session [\#325](https://github.com/omab/python-social-auth/issues/325) + +## [v0.2.5](https://github.com/omab/python-social-auth/tree/v0.2.5) (2015-04-13) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.4...v0.2.5) + +**Closed issues:** + +- Setting user.is\_active to false at end of pipeline logs out user [\#586](https://github.com/omab/python-social-auth/issues/586) + +## [v0.2.4](https://github.com/omab/python-social-auth/tree/v0.2.4) (2015-04-12) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.3...v0.2.4) + +**Closed issues:** + +- djangopackages.com still referes to the old project \(django-social-auth\) [\#585](https://github.com/omab/python-social-auth/issues/585) +- warnings in Django 1.8 [\#584](https://github.com/omab/python-social-auth/issues/584) +- Problems with upgrading Django Packages to python-social-auth [\#582](https://github.com/omab/python-social-auth/issues/582) +- Django 1.9 Warnings [\#581](https://github.com/omab/python-social-auth/issues/581) +- django 1.8, ImportError: No module named social\_auth.context\_processors [\#579](https://github.com/omab/python-social-auth/issues/579) +- sdist tarball is missing some files and dirs [\#578](https://github.com/omab/python-social-auth/issues/578) +- Using twitter backend with mongoengine [\#576](https://github.com/omab/python-social-auth/issues/576) +- Issues while using Custom User model [\#575](https://github.com/omab/python-social-auth/issues/575) +- Throw a more helpful exception when oauth\_consumer\_key is missing for OAuth1 [\#574](https://github.com/omab/python-social-auth/issues/574) +- sqlalchemy\_orm: ImportError: No module named transaction [\#572](https://github.com/omab/python-social-auth/issues/572) +- New version ? [\#571](https://github.com/omab/python-social-auth/issues/571) +- logout without disconnect [\#568](https://github.com/omab/python-social-auth/issues/568) +- SSL issue with google oauth2 [\#566](https://github.com/omab/python-social-auth/issues/566) +- next parameter containing get parameters [\#565](https://github.com/omab/python-social-auth/issues/565) +- get\(\) returned more than one UserSocialAuth -- it returned 2! [\#553](https://github.com/omab/python-social-auth/issues/553) +- RemovedInDjango19Warning [\#551](https://github.com/omab/python-social-auth/issues/551) +- Development/debug option to stub backend while developing [\#546](https://github.com/omab/python-social-auth/issues/546) +- weibo access\_token ajax auth fail [\#532](https://github.com/omab/python-social-auth/issues/532) +- Change PyJWT dependency version in setup.py from PyJWT==0.4.1 to PyJWT\>=0.4.1 [\#531](https://github.com/omab/python-social-auth/issues/531) +- Behance authentication [\#530](https://github.com/omab/python-social-auth/issues/530) +- upstream sent too big header while reading response header from upstream [\#527](https://github.com/omab/python-social-auth/issues/527) +- Fails to work with Django 1.8 [\#526](https://github.com/omab/python-social-auth/issues/526) +- AttributeError in VKOAuth2 [\#525](https://github.com/omab/python-social-auth/issues/525) +- Login user with Email address instead of Username [\#513](https://github.com/omab/python-social-auth/issues/513) +- Actually Log Exceptions in SocialAuthExceptionMiddleware [\#507](https://github.com/omab/python-social-auth/issues/507) +- Don't require trailing slashes [\#505](https://github.com/omab/python-social-auth/issues/505) +- django example [\#504](https://github.com/omab/python-social-auth/issues/504) +- complete/mendeley-oauth2 not successful [\#501](https://github.com/omab/python-social-auth/issues/501) +- Unable to refresh google oauth2 token after update python social auth to 0.2.1 [\#485](https://github.com/omab/python-social-auth/issues/485) +- revoke\_token\_params & revoke\_token\_headers are missing for GooglePlusAuth [\#484](https://github.com/omab/python-social-auth/issues/484) +- Microsoft Live Oauth2 Error [\#483](https://github.com/omab/python-social-auth/issues/483) +- Support Facebook Graph API 2.2 [\#480](https://github.com/omab/python-social-auth/issues/480) +- Spotify setting names are incorrect. [\#475](https://github.com/omab/python-social-auth/issues/475) +- Django adds migration [\#474](https://github.com/omab/python-social-auth/issues/474) +- SOCIAL\_AUTH\_LINKEDIN\_FIELD\_OAUTH2\_SELECTORS Not being used to populate user creation backend [\#466](https://github.com/omab/python-social-auth/issues/466) +- Yahoo OAuth 2? [\#463](https://github.com/omab/python-social-auth/issues/463) +- Docs for SOCIAL\_AUTH\_PROTECTED\_USER\_FIELDS misleading [\#459](https://github.com/omab/python-social-auth/issues/459) +- Gracefully handle AuthExceptions [\#458](https://github.com/omab/python-social-auth/issues/458) +- why context processor replace hyphen by underscore in google-oauth2 ? [\#457](https://github.com/omab/python-social-auth/issues/457) +- On linkedin,github login: AttributeError at http://llovebaimuda.herokuapp.com:8000/complete/github/ 'GithubBackend' object has no attribute 'auth\_allowed' [\#442](https://github.com/omab/python-social-auth/issues/442) +- GET /disconnect/\/ HTTP/1.0" 405 [\#438](https://github.com/omab/python-social-auth/issues/438) +- Facebook api change [\#424](https://github.com/omab/python-social-auth/issues/424) +- Import error: no module named google\_auth [\#423](https://github.com/omab/python-social-auth/issues/423) +- Django: Google+ disconnect does not actually disconnect [\#394](https://github.com/omab/python-social-auth/issues/394) +- How to save user to db without 'request' in register\_by\_access\_token\(request, backend\) function? [\#393](https://github.com/omab/python-social-auth/issues/393) +- Support Paste style configuration [\#392](https://github.com/omab/python-social-auth/issues/392) +- Google OAuth2 gives 400 error, FB 500 error [\#364](https://github.com/omab/python-social-auth/issues/364) +- Django - Google Authentication - Create Account [\#362](https://github.com/omab/python-social-auth/issues/362) +- Make Migrations Backward-Compatible with South [\#353](https://github.com/omab/python-social-auth/issues/353) +- Github access\_token never stored [\#344](https://github.com/omab/python-social-auth/issues/344) +- How to extends django orm mixins [\#343](https://github.com/omab/python-social-auth/issues/343) +- a few issues [\#333](https://github.com/omab/python-social-auth/issues/333) +- south migration for django app? [\#331](https://github.com/omab/python-social-auth/issues/331) +- Cannot log out from GooglePlus Auth. Homepage keeps calling its GooglePlus callback [\#316](https://github.com/omab/python-social-auth/issues/316) +- change log [\#313](https://github.com/omab/python-social-auth/issues/313) +- Return 503 instead of raise 500 error when auth provider not accessible [\#304](https://github.com/omab/python-social-auth/issues/304) +- Facebook SOCIAL\_AUTH\_FACEBOOK\_SCOPE not working as expected [\#294](https://github.com/omab/python-social-auth/issues/294) +- Add Django 1.7 migrations [\#270](https://github.com/omab/python-social-auth/issues/270) +- IntegrityError at /social/complete/facebook/ duplicate key value violates unique constraint "userprofile\_user\_email\_key" [\#208](https://github.com/omab/python-social-auth/issues/208) + +**Merged pull requests:** + +- Build a wheel, and upload with twine [\#583](https://github.com/omab/python-social-auth/pull/583) ([mattrobenolt](https://github.com/mattrobenolt)) +- Allow inactive users to login [\#580](https://github.com/omab/python-social-auth/pull/580) ([LucasRoesler](https://github.com/LucasRoesler)) +- Update LICENSE [\#573](https://github.com/omab/python-social-auth/pull/573) ([yasoob](https://github.com/yasoob)) + +## [v0.2.3](https://github.com/omab/python-social-auth/tree/v0.2.3) (2015-03-31) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.2...v0.2.3) + +**Closed issues:** + +- get\_username as a classmethod collides with the standard django implementation and other packages [\#564](https://github.com/omab/python-social-auth/issues/564) +- Make it easier to disable social\_details pipeline step [\#555](https://github.com/omab/python-social-auth/issues/555) +- Not compatible with requests-oauthlib 0.3.0 [\#545](https://github.com/omab/python-social-auth/issues/545) +- how to remove "redirect\_state" params? \( Kakao OAuth2 Error \) [\#538](https://github.com/omab/python-social-auth/issues/538) +- async interface for models in tornado [\#535](https://github.com/omab/python-social-auth/issues/535) +- `social.strategies.django\_strategy` work with django 1.7.4 "This QueryDict instance is immutable" [\#528](https://github.com/omab/python-social-auth/issues/528) +- Update PyPI [\#523](https://github.com/omab/python-social-auth/issues/523) +- Missing migration [\#516](https://github.com/omab/python-social-auth/issues/516) +- Not getting correct GoogleOath2 details when signing up by OAuth access token [\#499](https://github.com/omab/python-social-auth/issues/499) +- Jawbone backend problem, AuthCanceled exception. [\#497](https://github.com/omab/python-social-auth/issues/497) +- StravaOAuth - Strava authentication backend not working. [\#455](https://github.com/omab/python-social-auth/issues/455) + +**Merged pull requests:** + +- Added NaszaKlasa OAuth2 support [\#570](https://github.com/omab/python-social-auth/pull/570) ([hoffmannkrzysztof](https://github.com/hoffmannkrzysztof)) +- Add revoke token ability to strava [\#569](https://github.com/omab/python-social-auth/pull/569) ([buddylindsey](https://github.com/buddylindsey)) +- set redirect\_state to false for live oauth2 [\#563](https://github.com/omab/python-social-auth/pull/563) ([wj1918](https://github.com/wj1918)) +- Khan academy backend user\_id is required to use any further requests [\#561](https://github.com/omab/python-social-auth/pull/561) ([aniav](https://github.com/aniav)) +- Rednose and config [\#560](https://github.com/omab/python-social-auth/pull/560) ([jeromelefeuvre](https://github.com/jeromelefeuvre)) +- Add missing migration for Django app [\#558](https://github.com/omab/python-social-auth/pull/558) ([andreipetre](https://github.com/andreipetre)) +- Require PyJWT\>=1.0.0,\<2.0.0 [\#557](https://github.com/omab/python-social-auth/pull/557) ([jpadilla](https://github.com/jpadilla)) +- Start pipeline with default details arg [\#556](https://github.com/omab/python-social-auth/pull/556) ([johtso](https://github.com/johtso)) +- Add `python\_chameleon` to setup [\#554](https://github.com/omab/python-social-auth/pull/554) ([jeromelefeuvre](https://github.com/jeromelefeuvre)) +- update for django 1.9 [\#550](https://github.com/omab/python-social-auth/pull/550) ([DanielJDufour](https://github.com/DanielJDufour)) +- Added support for Vend [\#549](https://github.com/omab/python-social-auth/pull/549) ([matthowland](https://github.com/matthowland)) +- Increase min request-oauthlib version to 0.3.1 [\#548](https://github.com/omab/python-social-auth/pull/548) ([johtso](https://github.com/johtso)) +- Add wunderlist backend to the list [\#547](https://github.com/omab/python-social-auth/pull/547) ([bogdal](https://github.com/bogdal)) +- Typo in index.html [\#544](https://github.com/omab/python-social-auth/pull/544) ([flesser](https://github.com/flesser)) +- Wunderlist oauth2 backend [\#543](https://github.com/omab/python-social-auth/pull/543) ([bogdal](https://github.com/bogdal)) +- Add backend for EVE Online Single Sign-On \(OAuth2\) [\#541](https://github.com/omab/python-social-auth/pull/541) ([flesser](https://github.com/flesser)) +- Add extra info on Google+ Sign-In doc [\#540](https://github.com/omab/python-social-auth/pull/540) ([Menda](https://github.com/Menda)) +- fix issue \#538 : disable redirect\_state on KakaoOAuth2 [\#539](https://github.com/omab/python-social-auth/pull/539) ([dobestan](https://github.com/dobestan)) +- Update google.rst [\#537](https://github.com/omab/python-social-auth/pull/537) ([tclancy](https://github.com/tclancy)) +- Added Yahoo OAuth2 support [\#536](https://github.com/omab/python-social-auth/pull/536) ([hassek](https://github.com/hassek)) +- Fix Issue \#532 [\#533](https://github.com/omab/python-social-auth/pull/533) ([littlezz](https://github.com/littlezz)) + +## [v0.2.2](https://github.com/omab/python-social-auth/tree/v0.2.2) (2015-02-23) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.1...v0.2.2) + +**Closed issues:** + +- Problem with REQUEST in Django 1.7 [\#508](https://github.com/omab/python-social-auth/issues/508) +- Unique constraint on nonce missing [\#490](https://github.com/omab/python-social-auth/issues/490) +- AuthStateMissing [\#462](https://github.com/omab/python-social-auth/issues/462) +- \.\_\_proxy\_\_ object at 0x7feb8a56f5f8\> is not JSON serializable [\#460](https://github.com/omab/python-social-auth/issues/460) +- Cannot import name migrations [\#456](https://github.com/omab/python-social-auth/issues/456) +- Bugs in tornado\_strategy.py [\#445](https://github.com/omab/python-social-auth/issues/445) +- Cannot login with social account anymore after migration from DSA \(but association is OK\) [\#444](https://github.com/omab/python-social-auth/issues/444) +- Mode debug [\#443](https://github.com/omab/python-social-auth/issues/443) +- On linkedin,github login: AttributeError at http://llovebaimuda.herokuapp.com:8000/complete/github/ 'GithubBackend' object has no attribute 'auth\_allowed' [\#441](https://github.com/omab/python-social-auth/issues/441) +- Can not migrate database with django 1.7 [\#439](https://github.com/omab/python-social-auth/issues/439) +- AuthAlreadyAssociated at /complete/google-oauth2/ [\#437](https://github.com/omab/python-social-auth/issues/437) +- Python social auth redirect to LOGIN\_ERROR [\#435](https://github.com/omab/python-social-auth/issues/435) +- Can't register user when using email as username [\#434](https://github.com/omab/python-social-auth/issues/434) +- Problems connecting Google OAUTH2 [\#433](https://github.com/omab/python-social-auth/issues/433) +- Can't get refresh\_token from google-oauth2 response [\#431](https://github.com/omab/python-social-auth/issues/431) +- UserMixin.tokens naming [\#430](https://github.com/omab/python-social-auth/issues/430) +- Django 1.7 Type Object 'Migration' has no Attribute 'models' [\#427](https://github.com/omab/python-social-auth/issues/427) +- Django 1.7 warning - You have unapplied migrations [\#426](https://github.com/omab/python-social-auth/issues/426) +- Should the Django app's auth view be cacheable? [\#425](https://github.com/omab/python-social-auth/issues/425) +- changelog 0.2.1 only vs. releases on github 0.2.0 only [\#421](https://github.com/omab/python-social-auth/issues/421) +- Don't know how to redirect to right redirect\_uri. Use gunicorn + nginx + django1.7 [\#420](https://github.com/omab/python-social-auth/issues/420) +- Request object not passed in pipeline [\#419](https://github.com/omab/python-social-auth/issues/419) +- How to save the user data.? [\#418](https://github.com/omab/python-social-auth/issues/418) +- GitHub doesn't select Primary Email [\#413](https://github.com/omab/python-social-auth/issues/413) +- Unwanted and forced use of Google+ API for signin [\#406](https://github.com/omab/python-social-auth/issues/406) +- Renaming social url namespace [\#399](https://github.com/omab/python-social-auth/issues/399) +- How to overwrite redirect\_uri? [\#383](https://github.com/omab/python-social-auth/issues/383) +- GoogleOauth2 hangs mod\_wsgi after multiple logins. [\#377](https://github.com/omab/python-social-auth/issues/377) +- Facebook /login/facebook-app/ printing None [\#376](https://github.com/omab/python-social-auth/issues/376) +- How to test a custom python-social-auth pipeline? [\#352](https://github.com/omab/python-social-auth/issues/352) +- Cannot figure out how to associate multiple auth providers [\#340](https://github.com/omab/python-social-auth/issues/340) +- No user param in partial pipeline function [\#323](https://github.com/omab/python-social-auth/issues/323) + +**Merged pull requests:** + +- Fix example of pyramid [\#529](https://github.com/omab/python-social-auth/pull/529) ([narusemotoki](https://github.com/narusemotoki)) +- fix python3 handling of openid backend on sqlalchemy storage [\#524](https://github.com/omab/python-social-auth/pull/524) ([ghost](https://github.com/ghost)) +- Don't use "import" in example method paths docs to avoid confusion [\#521](https://github.com/omab/python-social-auth/pull/521) ([lamby](https://github.com/lamby)) +- Add dribbble backend. [\#519](https://github.com/omab/python-social-auth/pull/519) ([tell-k](https://github.com/tell-k)) +- Fixed issue: GET dictionary is immutable. [\#518](https://github.com/omab/python-social-auth/pull/518) ([baroale](https://github.com/baroale)) +- Include username in Reddit extra\_data [\#517](https://github.com/omab/python-social-auth/pull/517) ([chris-martin](https://github.com/chris-martin)) +- \[facebook-oauth2\] Verifying Graph API Calls with appsecret\_proof [\#515](https://github.com/omab/python-social-auth/pull/515) ([eagafonov](https://github.com/eagafonov)) +- Add Zotero Backend [\#514](https://github.com/omab/python-social-auth/pull/514) ([cdeblois](https://github.com/cdeblois)) +- add qiita backend [\#512](https://github.com/omab/python-social-auth/pull/512) ([tell-k](https://github.com/tell-k)) +- Fix: Issue \#508 [\#511](https://github.com/omab/python-social-auth/pull/511) ([baroale](https://github.com/baroale)) +- Fix Google documentation [\#510](https://github.com/omab/python-social-auth/pull/510) ([Menda](https://github.com/Menda)) +- Updated PyJWT Dependency [\#509](https://github.com/omab/python-social-auth/pull/509) ([clintonb](https://github.com/clintonb)) +- Ensure email is not None [\#503](https://github.com/omab/python-social-auth/pull/503) ([ianw](https://github.com/ianw)) +- Pull Request for \#501 [\#502](https://github.com/omab/python-social-auth/pull/502) ([cdeblois](https://github.com/cdeblois)) +- Add support for Launchpad OpenId [\#500](https://github.com/omab/python-social-auth/pull/500) ([ianw](https://github.com/ianw)) +- Jawbone authentification fix [\#498](https://github.com/omab/python-social-auth/pull/498) ([rivf](https://github.com/rivf)) +- Coursera backend [\#496](https://github.com/omab/python-social-auth/pull/496) ([dreame4](https://github.com/dreame4)) +- Added nonce unique constraint [\#491](https://github.com/omab/python-social-auth/pull/491) ([candlejack297](https://github.com/candlejack297)) +- Store Spotify's refresh\_token. [\#482](https://github.com/omab/python-social-auth/pull/482) ([ctbarna](https://github.com/ctbarna)) +- Slack improvements [\#479](https://github.com/omab/python-social-auth/pull/479) ([gorillamania](https://github.com/gorillamania)) +- Fixed extra\_data field in django 1.7 initial migration [\#476](https://github.com/omab/python-social-auth/pull/476) ([bendavis78](https://github.com/bendavis78)) +- YahooOAuth failed to get primary email if multiple email found in the profile. [\#473](https://github.com/omab/python-social-auth/pull/473) ([wj1918](https://github.com/wj1918)) +- Update base.py [\#472](https://github.com/omab/python-social-auth/pull/472) ([travoltino](https://github.com/travoltino)) +- Slack backend [\#471](https://github.com/omab/python-social-auth/pull/471) ([gorillamania](https://github.com/gorillamania)) +- Update GitHub documentation [\#469](https://github.com/omab/python-social-auth/pull/469) ([alexmuller](https://github.com/alexmuller)) +- Fix \#460: Call force\_text on \_URL settings to support reverse\_lazy with default session serializer [\#468](https://github.com/omab/python-social-auth/pull/468) ([frankier](https://github.com/frankier)) +- Update Django instructions to fix South migrations [\#454](https://github.com/omab/python-social-auth/pull/454) ([drpancake](https://github.com/drpancake)) +- Added backend for professionali.ru [\#452](https://github.com/omab/python-social-auth/pull/452) ([kblw](https://github.com/kblw)) +- Removed Orkut backend [\#450](https://github.com/omab/python-social-auth/pull/450) ([lukasklein](https://github.com/lukasklein)) +- Allow the pipeline to change the redirect url. [\#449](https://github.com/omab/python-social-auth/pull/449) ([tim-schilling](https://github.com/tim-schilling)) +- Added support for Django's User.EMAIL\_FIELD. [\#447](https://github.com/omab/python-social-auth/pull/447) ([SeanHayes](https://github.com/SeanHayes)) +- Khan Academy backend [\#446](https://github.com/omab/python-social-auth/pull/446) ([aniav](https://github.com/aniav)) +- Fix typo for AUTH\_USER\_MODEL [\#432](https://github.com/omab/python-social-auth/pull/432) ([jlynn](https://github.com/jlynn)) +- Update base.py , removing unncessary code after refactoring [\#429](https://github.com/omab/python-social-auth/pull/429) ([aparij](https://github.com/aparij)) +- use correct tense for `to meet' [\#428](https://github.com/omab/python-social-auth/pull/428) ([mgalgs](https://github.com/mgalgs)) +- Fix custom user model migrations for Django 1.7 [\#422](https://github.com/omab/python-social-auth/pull/422) ([jlynn](https://github.com/jlynn)) +- Fix migration issue on python 3 [\#417](https://github.com/omab/python-social-auth/pull/417) ([EnTeQuAk](https://github.com/EnTeQuAk)) +- Fix does not match the number of arguments \(for vk and ok backend\) [\#415](https://github.com/omab/python-social-auth/pull/415) ([silentsokolov](https://github.com/silentsokolov)) +- Salesforce OAuth2 support [\#412](https://github.com/omab/python-social-auth/pull/412) ([postrational](https://github.com/postrational)) +- Thedrow patch 1 [\#411](https://github.com/omab/python-social-auth/pull/411) ([omab](https://github.com/omab)) +- Added Python 3.4 and PyPy to the build matrix. [\#410](https://github.com/omab/python-social-auth/pull/410) ([thedrow](https://github.com/thedrow)) +- Added Django 1.7 App Config [\#409](https://github.com/omab/python-social-auth/pull/409) ([micahhausler](https://github.com/micahhausler)) +- Django admin enhancements [\#408](https://github.com/omab/python-social-auth/pull/408) ([micahhausler](https://github.com/micahhausler)) +- Use new GoogleOAuth2 Spec [\#407](https://github.com/omab/python-social-auth/pull/407) ([jaitaiwan](https://github.com/jaitaiwan)) +- \[flask\_example\_app\]: Incorrect import path for db model [\#405](https://github.com/omab/python-social-auth/pull/405) ([labeneator](https://github.com/labeneator)) +- Add Kakao link and detailed address for description. [\#403](https://github.com/omab/python-social-auth/pull/403) ([jeyraof](https://github.com/jeyraof)) +- Added some legal stuff [\#402](https://github.com/omab/python-social-auth/pull/402) ([dzerrenner](https://github.com/dzerrenner)) +- Recreate migration with Django 1.7 final and re-PEP8. [\#401](https://github.com/omab/python-social-auth/pull/401) ([akx](https://github.com/akx)) +- master add SCOPE\_SEPARATOR to DisqusOAuth2 [\#398](https://github.com/omab/python-social-auth/pull/398) ([ctrl-alt-delete](https://github.com/ctrl-alt-delete)) +- added a backend for Battle.net Oauth2 auth [\#397](https://github.com/omab/python-social-auth/pull/397) ([dzerrenner](https://github.com/dzerrenner)) +- Update documentation with info on upgrading from 0.1-0.2 with migrations [\#395](https://github.com/omab/python-social-auth/pull/395) ([timsavage](https://github.com/timsavage)) +- Allow more Trello settings [\#389](https://github.com/omab/python-social-auth/pull/389) ([sk7](https://github.com/sk7)) +- Updated to use latest api wrapper [\#386](https://github.com/omab/python-social-auth/pull/386) ([dhendo](https://github.com/dhendo)) +- updated the docs to add migrations for 1.7 while updated a constant so the warning message does not appear when running command line [\#382](https://github.com/omab/python-social-auth/pull/382) ([masterfung](https://github.com/masterfung)) +- Jawbone needs params instead of data as requests [\#380](https://github.com/omab/python-social-auth/pull/380) ([amolkher](https://github.com/amolkher)) +- Don't overwrite clean\_kwargs with kwargs [\#332](https://github.com/omab/python-social-auth/pull/332) ([cambridgemike](https://github.com/cambridgemike)) +- Reinstated get\_user\_id override [\#314](https://github.com/omab/python-social-auth/pull/314) ([dhendo](https://github.com/dhendo)) +- Update django\_orm.py [\#312](https://github.com/omab/python-social-auth/pull/312) ([synotna](https://github.com/synotna)) + +## [v0.2.1](https://github.com/omab/python-social-auth/tree/v0.2.1) (2014-09-11) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.0...v0.2.1) + +## [v0.2.0](https://github.com/omab/python-social-auth/tree/v0.2.0) (2014-09-11) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.26...v0.2.0) + +**Closed issues:** + +- cannot import name strategy [\#370](https://github.com/omab/python-social-auth/issues/370) +- Request object has no attribute 'backend' in SocialAuthExceptionMiddleware [\#369](https://github.com/omab/python-social-auth/issues/369) +- Shopify Backend [\#368](https://github.com/omab/python-social-auth/issues/368) +- State parameter incorrectly missing for some backends [\#367](https://github.com/omab/python-social-auth/issues/367) +- /signup/email/ instead of /login/email/ [\#366](https://github.com/omab/python-social-auth/issues/366) +- Improve pipeline documentation [\#361](https://github.com/omab/python-social-auth/issues/361) +- request: opposite default behavior for SOCIAL\_AUTH\_SESSION\_EXPIRATION [\#356](https://github.com/omab/python-social-auth/issues/356) +- after installing I have got an error: ImportError: No module named defaultqrcode [\#355](https://github.com/omab/python-social-auth/issues/355) +- SocialAuthExceptionMiddleware raises AttributeError [\#350](https://github.com/omab/python-social-auth/issues/350) +- Python 3 support [\#349](https://github.com/omab/python-social-auth/issues/349) +- can I set redirect\_uri use weibo backend? [\#345](https://github.com/omab/python-social-auth/issues/345) +- Toronodo-Facebook oauth [\#342](https://github.com/omab/python-social-auth/issues/342) +- Security issue with Twitter backend - state parameter [\#338](https://github.com/omab/python-social-auth/issues/338) +- \ [\#330](https://github.com/omab/python-social-auth/issues/330) +- Github support for checking if a user is part of a team [\#329](https://github.com/omab/python-social-auth/issues/329) +- Github OAuth2 backend fails with 404 when retrieving access token [\#327](https://github.com/omab/python-social-auth/issues/327) +- django admin User social auth search broken [\#322](https://github.com/omab/python-social-auth/issues/322) +- Script to migrate django sessions to python social auth [\#320](https://github.com/omab/python-social-auth/issues/320) +- Error with facebook login [\#315](https://github.com/omab/python-social-auth/issues/315) +- NotImplementedError [\#310](https://github.com/omab/python-social-auth/issues/310) +- No module named 'social\_auth' on social/utils.py [\#306](https://github.com/omab/python-social-auth/issues/306) +- What is the correct way to use get tokens with GooglePlus? [\#305](https://github.com/omab/python-social-auth/issues/305) +- custom LOGIN\_REDIRECT\_URL per backend [\#301](https://github.com/omab/python-social-auth/issues/301) +- Instagram has changed user data format [\#296](https://github.com/omab/python-social-auth/issues/296) +- Getting "cannot import name psa" error [\#295](https://github.com/omab/python-social-auth/issues/295) +- Broken partial auth with Django and 0.1.24 [\#291](https://github.com/omab/python-social-auth/issues/291) +- Google+ Sign-in problem [\#285](https://github.com/omab/python-social-auth/issues/285) +- AuthStateMissing: Session value state missing [\#279](https://github.com/omab/python-social-auth/issues/279) +- Always returns me detail: "Invalid token" [\#268](https://github.com/omab/python-social-auth/issues/268) +- On facebook login: AttributeError at /complete/facebook/ 'NoneType' object has no attribute 'expiration\_datetime' [\#190](https://github.com/omab/python-social-auth/issues/190) + +**Merged pull requests:** + +- Adds backend for MineID.org [\#379](https://github.com/omab/python-social-auth/pull/379) ([caioariede](https://github.com/caioariede)) +- Fix typo [\#372](https://github.com/omab/python-social-auth/pull/372) ([gipi](https://github.com/gipi)) +- Updated OpenId Connect Test Mixin [\#371](https://github.com/omab/python-social-auth/pull/371) ([clintonb](https://github.com/clintonb)) +- Small grammatical edit [\#363](https://github.com/omab/python-social-auth/pull/363) ([x0xMaximus](https://github.com/x0xMaximus)) +- Fix repository links in thanks document. [\#359](https://github.com/omab/python-social-auth/pull/359) ([martey](https://github.com/martey)) +- changed default behavior of SESSION\_EXPIRATION setting [\#358](https://github.com/omab/python-social-auth/pull/358) ([gameguy43](https://github.com/gameguy43)) +- added goclio oauth2 backend [\#357](https://github.com/omab/python-social-auth/pull/357) ([rosscdh](https://github.com/rosscdh)) +- Add pushbullet backends [\#351](https://github.com/omab/python-social-auth/pull/351) ([ralmn](https://github.com/ralmn)) +- Added Open ID Connect base backend [\#348](https://github.com/omab/python-social-auth/pull/348) ([clintonb](https://github.com/clintonb)) +- numeric index for format [\#347](https://github.com/omab/python-social-auth/pull/347) ([jprobst21](https://github.com/jprobst21)) +- Update vk.rst [\#341](https://github.com/omab/python-social-auth/pull/341) ([darthwade](https://github.com/darthwade)) +- Django \<1.7 Migration Support [\#339](https://github.com/omab/python-social-auth/pull/339) ([mhluongo](https://github.com/mhluongo)) +- Strava name population fixes [\#336](https://github.com/omab/python-social-auth/pull/336) ([lamby](https://github.com/lamby)) +- Correct Strava scoping/permissions example. [\#335](https://github.com/omab/python-social-auth/pull/335) ([lamby](https://github.com/lamby)) +- Clean up language in social/tests/README.rst [\#334](https://github.com/omab/python-social-auth/pull/334) ([chris-martin](https://github.com/chris-martin)) +- Fixed \#327 -- Changed access token method on backend. [\#328](https://github.com/omab/python-social-auth/pull/328) ([slurms](https://github.com/slurms)) +- Minor doc updates [\#326](https://github.com/omab/python-social-auth/pull/326) ([seizethedave](https://github.com/seizethedave)) +- fix for AssertionError in pyramid [\#319](https://github.com/omab/python-social-auth/pull/319) ([marinewater](https://github.com/marinewater)) +- Added Django 1.7 migrations [\#318](https://github.com/omab/python-social-auth/pull/318) ([ondrowan](https://github.com/ondrowan)) +- reddit sometimes responds with "429 Too Many Requests" seemingly randomly [\#317](https://github.com/omab/python-social-auth/pull/317) ([davidhubbard](https://github.com/davidhubbard)) +- Update link to Django example in documentation. [\#311](https://github.com/omab/python-social-auth/pull/311) ([martey](https://github.com/martey)) +- Add note about access\_type in docs [\#308](https://github.com/omab/python-social-auth/pull/308) ([romanlevin](https://github.com/romanlevin)) +- The Moves app backend [\#307](https://github.com/omab/python-social-auth/pull/307) ([avibrazil](https://github.com/avibrazil)) +- QQ backend [\#302](https://github.com/omab/python-social-auth/pull/302) ([omab](https://github.com/omab)) +- \[documentation\] text should not go into code block [\#299](https://github.com/omab/python-social-auth/pull/299) ([GabLeRoux](https://github.com/GabLeRoux)) +- Vkotnakte [\#298](https://github.com/omab/python-social-auth/pull/298) ([freydev](https://github.com/freydev)) +- Update docker backend with Docker Hub endpoints [\#293](https://github.com/omab/python-social-auth/pull/293) ([jlhawn](https://github.com/jlhawn)) + +## [v0.1.26](https://github.com/omab/python-social-auth/tree/v0.1.26) (2014-06-07) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.25...v0.1.26) + +**Closed issues:** + +- Google OAuth2 broken since 0.1.24 [\#292](https://github.com/omab/python-social-auth/issues/292) + +## [v0.1.25](https://github.com/omab/python-social-auth/tree/v0.1.25) (2014-06-07) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.24...v0.1.25) + +**Closed issues:** + +- LinkedIn-OAuth2 refresh\_token doesn't work [\#289](https://github.com/omab/python-social-auth/issues/289) +- Process exceptions even when DEBUG = True [\#287](https://github.com/omab/python-social-auth/issues/287) +- python-openid does not support py3k. Alternatives? [\#282](https://github.com/omab/python-social-auth/issues/282) +- Twitter OAuth using access\_token [\#272](https://github.com/omab/python-social-auth/issues/272) + +**Merged pull requests:** + +- Rdio API methods use POST [\#288](https://github.com/omab/python-social-auth/pull/288) ([dasevilla](https://github.com/dasevilla)) +- Fixed Django 1.7 admin [\#286](https://github.com/omab/python-social-auth/pull/286) ([godshall](https://github.com/godshall)) +- avoid updating default settings [\#281](https://github.com/omab/python-social-auth/pull/281) ([l-hedgehog](https://github.com/l-hedgehog)) + +## [v0.1.24](https://github.com/omab/python-social-auth/tree/v0.1.24) (2014-05-18) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.23...v0.1.24) + +**Closed issues:** + +- Facebook2OAuth2 not setting 'client\_id' parameter when redirecting to login [\#280](https://github.com/omab/python-social-auth/issues/280) +- Wrong version on pip [\#277](https://github.com/omab/python-social-auth/issues/277) +- SOCIAL\_AUTH\_NEW\_USER\_REDIRECT\_URL strange behaviour [\#276](https://github.com/omab/python-social-auth/issues/276) +- Feature request: Ability to encrypt access tokens [\#274](https://github.com/omab/python-social-auth/issues/274) +- Google is deprecating some OAuth scopes [\#273](https://github.com/omab/python-social-auth/issues/273) +- 'RegexURLResolver' object has no attribute '\_urlconf\_module' [\#269](https://github.com/omab/python-social-auth/issues/269) +- I am facing a 500: Internal Server Error after clicking any link [\#266](https://github.com/omab/python-social-auth/issues/266) +- EXTRA\_DATA for VK [\#263](https://github.com/omab/python-social-auth/issues/263) +- Amazon Docs Out of Date? [\#260](https://github.com/omab/python-social-auth/issues/260) +- Strava Integration OAuth Redirect Issue [\#259](https://github.com/omab/python-social-auth/issues/259) +- Add the ability to customize AX\_SCHEMA\_ATTRS [\#258](https://github.com/omab/python-social-auth/issues/258) +- g.user may be Proxy! \(flask bug\) [\#257](https://github.com/omab/python-social-auth/issues/257) +- Error running syncdb with MySQL utf8mb4 charset [\#255](https://github.com/omab/python-social-auth/issues/255) +- Use of Django Internal Property [\#254](https://github.com/omab/python-social-auth/issues/254) +- Persona auth failing to authenticate when using a custom user model \[Django\] [\#253](https://github.com/omab/python-social-auth/issues/253) +- Facebook: "Invalid App ID: None" [\#252](https://github.com/omab/python-social-auth/issues/252) +- "vk-openapi" backend error [\#250](https://github.com/omab/python-social-auth/issues/250) +- request hanging after social authentication [\#248](https://github.com/omab/python-social-auth/issues/248) +- Unable To Redirect User After Facebook Authentication [\#247](https://github.com/omab/python-social-auth/issues/247) +- social.exceptions.AuthStateMissing [\#244](https://github.com/omab/python-social-auth/issues/244) +- Facebook Re-authentication [\#243](https://github.com/omab/python-social-auth/issues/243) +- SOCIAL\_AUTH\_DEFAULT\_USERNAME [\#241](https://github.com/omab/python-social-auth/issues/241) +- Refactor first, last and full name population [\#240](https://github.com/omab/python-social-auth/issues/240) +- Authication using acees token works for facebook but not twitter and VK [\#238](https://github.com/omab/python-social-auth/issues/238) +- MendeleyOAuth2 does not require REDIRECT\_STATE [\#234](https://github.com/omab/python-social-auth/issues/234) +- Autnticate/Create user from acces\_token [\#233](https://github.com/omab/python-social-auth/issues/233) +- Problem using @partial with GoogleOpenId in Django 1.6 and python 3.3 [\#231](https://github.com/omab/python-social-auth/issues/231) +- Unicode error with UTF-8 string in next [\#229](https://github.com/omab/python-social-auth/issues/229) +- Error with "Enhanced redirection security" in Microsoft account \(Live backend\). [\#218](https://github.com/omab/python-social-auth/issues/218) + +**Merged pull requests:** + +- Implementing Spotify and Beats OAuth implementations. [\#283](https://github.com/omab/python-social-auth/pull/283) ([ryankicks](https://github.com/ryankicks)) +- Add MapMyFitness [\#278](https://github.com/omab/python-social-auth/pull/278) ([JasonSanford](https://github.com/JasonSanford)) +- from http API to https API [\#275](https://github.com/omab/python-social-auth/pull/275) ([swmerko](https://github.com/swmerko)) +- Replace references to python-oauth2 with references to requests-oauthlib [\#271](https://github.com/omab/python-social-auth/pull/271) ([malept](https://github.com/malept)) +- get email on login via VK [\#267](https://github.com/omab/python-social-auth/pull/267) ([Smamaxs](https://github.com/Smamaxs)) +- Change the authorization url for the xing api [\#265](https://github.com/omab/python-social-auth/pull/265) ([hujiko](https://github.com/hujiko)) +- Support for Facebook Open Graph 2.0 [\#264](https://github.com/omab/python-social-auth/pull/264) ([dryan](https://github.com/dryan)) +- Added LoginRadius backend. [\#262](https://github.com/omab/python-social-auth/pull/262) ([grepme](https://github.com/grepme)) +- Add Kakao backend [\#261](https://github.com/omab/python-social-auth/pull/261) ([momamene](https://github.com/momamene)) +- Using https as required by the API [\#256](https://github.com/omab/python-social-auth/pull/256) ([gmist](https://github.com/gmist)) +- User model fields accessors clashes issue solved [\#251](https://github.com/omab/python-social-auth/pull/251) ([wumzi](https://github.com/wumzi)) +- linkedin now requires redirect uris to be verified: https://developer.li... [\#246](https://github.com/omab/python-social-auth/pull/246) ([dblado](https://github.com/dblado)) +- Add Twitch backend [\#245](https://github.com/omab/python-social-auth/pull/245) ([hannseman](https://github.com/hannseman)) +- Handle properly refusing when entering via twitter [\#242](https://github.com/omab/python-social-auth/pull/242) ([Chern](https://github.com/Chern)) +- Fix small spelling mistake. [\#239](https://github.com/omab/python-social-auth/pull/239) ([cdepillabout](https://github.com/cdepillabout)) +- Add support for Vimeo OAuth 2 as part of Vimeo API v3 [\#237](https://github.com/omab/python-social-auth/pull/237) ([jjshabs](https://github.com/jjshabs)) +- Update settings.rst [\#236](https://github.com/omab/python-social-auth/pull/236) ([krishangupta](https://github.com/krishangupta)) +- Incorrect syntax given in the documention [\#235](https://github.com/omab/python-social-auth/pull/235) ([mdamien](https://github.com/mdamien)) +- login with bitbucket account, error when any verified email is set [\#230](https://github.com/omab/python-social-auth/pull/230) ([pekoslaw](https://github.com/pekoslaw)) + +## [v0.1.23](https://github.com/omab/python-social-auth/tree/v0.1.23) (2014-03-26) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.22...v0.1.23) + +**Closed issues:** + +- Handling AuthAlreadyAssociated [\#226](https://github.com/omab/python-social-auth/issues/226) +- AuthFailed at /complete/dropbox-oauth2/ [\#225](https://github.com/omab/python-social-auth/issues/225) +- Github says 404 when I want to use it [\#224](https://github.com/omab/python-social-auth/issues/224) +- Dropbox OAuth2 backend not found [\#223](https://github.com/omab/python-social-auth/issues/223) +- EmailAuth pipeline - saving password [\#222](https://github.com/omab/python-social-auth/issues/222) +- SocialAuthExceptionMiddleware is not thread safe. [\#221](https://github.com/omab/python-social-auth/issues/221) +- `AuthStateMissing` and `HTTPError` being raised [\#220](https://github.com/omab/python-social-auth/issues/220) +- Saving to session and access after pipeline [\#219](https://github.com/omab/python-social-auth/issues/219) +- Migrating from Django social auth [\#214](https://github.com/omab/python-social-auth/issues/214) +- No module named apps.django\_app.default.models in custom pipeline [\#213](https://github.com/omab/python-social-auth/issues/213) +- complete login with facebook app [\#212](https://github.com/omab/python-social-auth/issues/212) +- Use Django messages in SocialAuthExceptionMiddleware even for anonymous users [\#210](https://github.com/omab/python-social-auth/issues/210) +- HTTPError at /complete/facebook/ when trying to connect to Facebook. [\#207](https://github.com/omab/python-social-auth/issues/207) +- Steam backend not using stateless mode [\#200](https://github.com/omab/python-social-auth/issues/200) +- Got UnicodeEncodeError when redirection parameter &next=/apage/contains/inτερnαtιοnal/characters/ [\#191](https://github.com/omab/python-social-auth/issues/191) +- PIP install to virtual environment fails [\#177](https://github.com/omab/python-social-auth/issues/177) +- AUTHENTICATION\_BACKENDS [\#131](https://github.com/omab/python-social-auth/issues/131) + +**Merged pull requests:** + +- Added backend for Last.Fm. [\#232](https://github.com/omab/python-social-auth/pull/232) ([eriklavander](https://github.com/eriklavander)) +- Added Docker.io backend [\#228](https://github.com/omab/python-social-auth/pull/228) ([fermayo](https://github.com/fermayo)) +- OpenStreetMap: no img element if user has no avatar [\#227](https://github.com/omab/python-social-auth/pull/227) ([yohanboniface](https://github.com/yohanboniface)) +- Added support for strava [\#217](https://github.com/omab/python-social-auth/pull/217) ([abunsen](https://github.com/abunsen)) +- Removes flask dependency from webpy\_app [\#216](https://github.com/omab/python-social-auth/pull/216) ([w0rm](https://github.com/w0rm)) +- Added backend for Ubuntu \(One\). [\#215](https://github.com/omab/python-social-auth/pull/215) ([schwuk](https://github.com/schwuk)) +- Fixed Django \< 1.4 support in context processors. [\#211](https://github.com/omab/python-social-auth/pull/211) ([bmispelon](https://github.com/bmispelon)) +- Add some missing test dependencies for `social.apps.django\_app.default.tests` [\#209](https://github.com/omab/python-social-auth/pull/209) ([pzrq](https://github.com/pzrq)) + +## [v0.1.22](https://github.com/omab/python-social-auth/tree/v0.1.22) (2014-03-01) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.21...v0.1.22) + +**Closed issues:** + +- Email confirmation is broken for SQLAlchemy storage and webpy\_app [\#204](https://github.com/omab/python-social-auth/issues/204) +- Associate by mail doesn't return is\_new flag [\#201](https://github.com/omab/python-social-auth/issues/201) +- Coinbase backend defaults to 'balance' but the complete calls user\_data with looks for /users api path [\#199](https://github.com/omab/python-social-auth/issues/199) +- Partial pipeline doesn't restore user model [\#198](https://github.com/omab/python-social-auth/issues/198) +- mongoengine should support USERNAME\_FIELD ? [\#197](https://github.com/omab/python-social-auth/issues/197) +- Action of do\_complete is not managing exceptions thrown during strategy.complete [\#196](https://github.com/omab/python-social-auth/issues/196) +- Using Django-facebook side by side [\#195](https://github.com/omab/python-social-auth/issues/195) +- Saving user as inactive in a pipeline, causes redirect to login error [\#194](https://github.com/omab/python-social-auth/issues/194) +- case-sensetive ?next= parameter dont work [\#193](https://github.com/omab/python-social-auth/issues/193) +- TypeError: can only concatenate list \(not "str"\) to list [\#186](https://github.com/omab/python-social-auth/issues/186) +- Post-authentication redirects: are they still supported? [\#182](https://github.com/omab/python-social-auth/issues/182) +- LinkedIn HTTPError: 401 Client Error: Unauthorized [\#181](https://github.com/omab/python-social-auth/issues/181) +- How to register user by access\_token [\#180](https://github.com/omab/python-social-auth/issues/180) +- Session value state missing [\#166](https://github.com/omab/python-social-auth/issues/166) +- Unavailable facebook raises unexpected ConnectionError [\#155](https://github.com/omab/python-social-auth/issues/155) +- Exceptions not noted in logs [\#154](https://github.com/omab/python-social-auth/issues/154) +- Internal Server Error: /complete/facebook/ -\> raise KeyError [\#153](https://github.com/omab/python-social-auth/issues/153) +- Migrating server [\#128](https://github.com/omab/python-social-auth/issues/128) +- django example: trying to get only the email auth work for now... [\#118](https://github.com/omab/python-social-auth/issues/118) +- Can we choose to set the login url escaped ? [\#115](https://github.com/omab/python-social-auth/issues/115) +- Incorporating rauth? [\#3](https://github.com/omab/python-social-auth/issues/3) + +**Merged pull requests:** + +- Fixes broken email confirmation for SQLAlchemy storage and webpy\_app [\#205](https://github.com/omab/python-social-auth/pull/205) ([w0rm](https://github.com/w0rm)) +- Update mendeley.py [\#203](https://github.com/omab/python-social-auth/pull/203) ([sbassi](https://github.com/sbassi)) +- Removed commit marker [\#192](https://github.com/omab/python-social-auth/pull/192) ([dkingman](https://github.com/dkingman)) +- Add Clef backend [\#189](https://github.com/omab/python-social-auth/pull/189) ([tklovett](https://github.com/tklovett)) +- Fixed a typo. [\#188](https://github.com/omab/python-social-auth/pull/188) ([ykalchevskiy](https://github.com/ykalchevskiy)) +- Add a Bitdeli Badge to README [\#185](https://github.com/omab/python-social-auth/pull/185) ([bitdeli-chef](https://github.com/bitdeli-chef)) +- added information for FIELDS\_STORED\_IN\_SESSION [\#184](https://github.com/omab/python-social-auth/pull/184) ([joelewis](https://github.com/joelewis)) +- updated live connection for better support [\#183](https://github.com/omab/python-social-auth/pull/183) ([hassek](https://github.com/hassek)) + +## [v0.1.21](https://github.com/omab/python-social-auth/tree/v0.1.21) (2014-02-05) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.20...v0.1.21) + +**Closed issues:** + +- User association by email should be case insensitive [\#179](https://github.com/omab/python-social-auth/issues/179) +- ImproperlyConfigured: Module "social.apps.django\_app.utils" does not define a "BackendWrapper" authentication backend [\#175](https://github.com/omab/python-social-auth/issues/175) +- Django usernames more then 30 charracters, via setting variable [\#174](https://github.com/omab/python-social-auth/issues/174) +- Dropbox Lack of Encoding Causes Connection Failures [\#173](https://github.com/omab/python-social-auth/issues/173) +- On new Tumblr login: AttributeError: 'NoneType' object has no attribute 'expiration\_datetime' [\#172](https://github.com/omab/python-social-auth/issues/172) +- Tornado example not working ? [\#171](https://github.com/omab/python-social-auth/issues/171) +- Unicode-object must be encoded before hashing [\#168](https://github.com/omab/python-social-auth/issues/168) +- Accessing access\_token ? [\#167](https://github.com/omab/python-social-auth/issues/167) +- suggestion: please change the username column in auth\_user from "name" to "domain" for weibo backend [\#164](https://github.com/omab/python-social-auth/issues/164) +- Invalid openid.mode: '\' [\#163](https://github.com/omab/python-social-auth/issues/163) +- get\_user\_id refers to details [\#136](https://github.com/omab/python-social-auth/issues/136) + +**Merged pull requests:** + +- Add version parameter to foursquare backend [\#176](https://github.com/omab/python-social-auth/pull/176) ([michisu](https://github.com/michisu)) +- Added PixelPin to list of providers [\#170](https://github.com/omab/python-social-auth/pull/170) ([lukos](https://github.com/lukos)) +- Added new PixelPin provider. [\#169](https://github.com/omab/python-social-auth/pull/169) ([lukos](https://github.com/lukos)) +- Serializer changed. [\#165](https://github.com/omab/python-social-auth/pull/165) ([omgbbqhaxx](https://github.com/omgbbqhaxx)) + +## [v0.1.20](https://github.com/omab/python-social-auth/tree/v0.1.20) (2014-01-17) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.19...v0.1.20) + +**Closed issues:** + +- docs and examples not included in pypi tarball [\#162](https://github.com/omab/python-social-auth/issues/162) +- Unable to retrieve any extra\_data from LinkedIn backend [\#161](https://github.com/omab/python-social-auth/issues/161) +- Twitter backend error with Python 3.3 [\#139](https://github.com/omab/python-social-auth/issues/139) + +## [v0.1.19](https://github.com/omab/python-social-auth/tree/v0.1.19) (2014-01-16) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.18...v0.1.19) + +## [v0.1.18](https://github.com/omab/python-social-auth/tree/v0.1.18) (2014-01-16) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.17...v0.1.18) + +**Closed issues:** + +- GooglePlusAuth backend do not store 'access\_token' on extra\_data \(psa v0.1.17\) [\#157](https://github.com/omab/python-social-auth/issues/157) +- partial pipeline "example.app.pipeline.require\_email" for django does not work [\#152](https://github.com/omab/python-social-auth/issues/152) +- Other dependencies missing [\#151](https://github.com/omab/python-social-auth/issues/151) +- Force https redirect\_uri causes Exception when loading strategy [\#148](https://github.com/omab/python-social-auth/issues/148) +- ValueError: too many values to unpack [\#146](https://github.com/omab/python-social-auth/issues/146) +- AuthCanceled: Authentication process canceled Error [\#144](https://github.com/omab/python-social-auth/issues/144) +- django nonce salt field is too short [\#141](https://github.com/omab/python-social-auth/issues/141) +- Missing dependencies in readme [\#140](https://github.com/omab/python-social-auth/issues/140) +- Incorrect Client Credentials via GitHub [\#138](https://github.com/omab/python-social-auth/issues/138) +- Could I redirect complete page to original login page? [\#137](https://github.com/omab/python-social-auth/issues/137) +- User-friendly backend names [\#132](https://github.com/omab/python-social-auth/issues/132) +- Yahoo backend handle key error [\#125](https://github.com/omab/python-social-auth/issues/125) +- Use constant time comparison function [\#122](https://github.com/omab/python-social-auth/issues/122) +- Inquiry: Why is social.tests.backends not part of the package? [\#119](https://github.com/omab/python-social-auth/issues/119) +- How to get Facebook username during Social authentication [\#117](https://github.com/omab/python-social-auth/issues/117) +- How to get backend instance [\#114](https://github.com/omab/python-social-auth/issues/114) +- Connecting multiple social auths from same provider [\#112](https://github.com/omab/python-social-auth/issues/112) +- Linkedin JSAPI and exchanging Client-side Bearer Token for OAuth 1.0a token [\#111](https://github.com/omab/python-social-auth/issues/111) +- get\_strategy\(\) got multiple values for keyword argument 'request' [\#110](https://github.com/omab/python-social-auth/issues/110) +- Twitter OAuth ValueError [\#107](https://github.com/omab/python-social-auth/issues/107) +- Facebook scope not set anymore [\#106](https://github.com/omab/python-social-auth/issues/106) +- Namespacing for python-social-auth [\#103](https://github.com/omab/python-social-auth/issues/103) +- Additional backend API calls after user authorization [\#102](https://github.com/omab/python-social-auth/issues/102) +- Linkedin OAuth not working [\#101](https://github.com/omab/python-social-auth/issues/101) +- Make extending SOCIAL\_AUTH\_PIPELINE easier [\#99](https://github.com/omab/python-social-auth/issues/99) +- Authentication problem with Weibo backend when integrate with Django application [\#98](https://github.com/omab/python-social-auth/issues/98) +- Odnoklassniki - PARAM\_API\_KEY : No application key [\#97](https://github.com/omab/python-social-auth/issues/97) +- Per backend FORCE\_EMAIL\_VALIDATION is not respected [\#95](https://github.com/omab/python-social-auth/issues/95) +- Migrating from django\_social\_auth [\#94](https://github.com/omab/python-social-auth/issues/94) +- UnicodeError in mailru backend [\#91](https://github.com/omab/python-social-auth/issues/91) +- setting OPENID\_PAPE\_MAX\_AUTH\_AGE equal to zero doesn't force reauthentication [\#89](https://github.com/omab/python-social-auth/issues/89) +- added associate\_by\_email to pipeline but still adding new account when i login with a social account [\#84](https://github.com/omab/python-social-auth/issues/84) +- LinkedIn OAuth2 bad request. [\#58](https://github.com/omab/python-social-auth/issues/58) + +**Merged pull requests:** + +- AUTHORIZATION\_URL changed to https [\#160](https://github.com/omab/python-social-auth/pull/160) ([harshiljain](https://github.com/harshiljain)) +- GooglePlusAuth backend do not store 'access\_token' on extra\_data \(psa v0.1.17\) [\#159](https://github.com/omab/python-social-auth/pull/159) ([jgsogo](https://github.com/jgsogo)) +- Solves some revoke\_token related errors \(BaseOAuth1 and FacebookOAuth2\) [\#158](https://github.com/omab/python-social-auth/pull/158) ([jgsogo](https://github.com/jgsogo)) +- odnoklassniki backend iframe app fix [\#156](https://github.com/omab/python-social-auth/pull/156) ([maxtepkeev](https://github.com/maxtepkeev)) +- Update Flask integration to most recent version [\#150](https://github.com/omab/python-social-auth/pull/150) ([xen](https://github.com/xen)) +- Fixed issue with redirect\_uri with https [\#149](https://github.com/omab/python-social-auth/pull/149) ([roberto-robles](https://github.com/roberto-robles)) +- add docs for backend Taobao [\#147](https://github.com/omab/python-social-auth/pull/147) ([jcouyang](https://github.com/jcouyang)) +- Add support for \(淘宝\)Taobao OAuth2 [\#145](https://github.com/omab/python-social-auth/pull/145) ([jcouyang](https://github.com/jcouyang)) +- Add Dropbox OAuth2 Support [\#143](https://github.com/omab/python-social-auth/pull/143) ([coddingtonbear](https://github.com/coddingtonbear)) +- increasing length of salt field for django apps, fixes \#141 [\#142](https://github.com/omab/python-social-auth/pull/142) ([eknuth](https://github.com/eknuth)) +- Add support for OpenStreetMap OAuth [\#135](https://github.com/omab/python-social-auth/pull/135) ([Xmypblu](https://github.com/Xmypblu)) +- Support for MongoEngine authentication using Custom User Model [\#134](https://github.com/omab/python-social-auth/pull/134) ([ncortot](https://github.com/ncortot)) +- Update reddit.py - comment was referencing Github. [\#133](https://github.com/omab/python-social-auth/pull/133) ([gorillamania](https://github.com/gorillamania)) +- Tiny typo fix [\#130](https://github.com/omab/python-social-auth/pull/130) ([parlarjb](https://github.com/parlarjb)) +- fix session expiration in vk backend [\#129](https://github.com/omab/python-social-auth/pull/129) ([maxtepkeev](https://github.com/maxtepkeev)) +- Added support for named URLs and URL translation using the django built-... [\#127](https://github.com/omab/python-social-auth/pull/127) ([hekevintran](https://github.com/hekevintran)) +- Updated pipeline example to include externalized auth. [\#126](https://github.com/omab/python-social-auth/pull/126) ([bimsapi](https://github.com/bimsapi)) +- Removed non-ascii character from author string [\#123](https://github.com/omab/python-social-auth/pull/123) ([monkut](https://github.com/monkut)) +- Add test backends to the package. [\#121](https://github.com/omab/python-social-auth/pull/121) ([hansl](https://github.com/hansl)) +- Missing trailing slash on complete url [\#120](https://github.com/omab/python-social-auth/pull/120) ([gorghoa](https://github.com/gorghoa)) +- getpocket.com backend [\#116](https://github.com/omab/python-social-auth/pull/116) ([stephenmcd](https://github.com/stephenmcd)) +- fix uid in coinbase oauth [\#109](https://github.com/omab/python-social-auth/pull/109) ([FloorLamp](https://github.com/FloorLamp)) +- Add Coinbase OAuth2 [\#105](https://github.com/omab/python-social-auth/pull/105) ([FloorLamp](https://github.com/FloorLamp)) +- Update weibo.py [\#100](https://github.com/omab/python-social-auth/pull/100) ([josseph](https://github.com/josseph)) +- Make vk-app backend to retrieve additional user data in respect to the \*\_EXTRA\_DATA setting [\#96](https://github.com/omab/python-social-auth/pull/96) ([maxtepkeev](https://github.com/maxtepkeev)) +- Refresh the docs on http://psa.matiasaguirre.net/docs/ [\#93](https://github.com/omab/python-social-auth/pull/93) ([sahilgupta](https://github.com/sahilgupta)) +- Allow for server side flow for Google+ [\#92](https://github.com/omab/python-social-auth/pull/92) ([assiotis](https://github.com/assiotis)) +- Fitbit uid [\#90](https://github.com/omab/python-social-auth/pull/90) ([juanriaza](https://github.com/juanriaza)) + +## [v0.1.17](https://github.com/omab/python-social-auth/tree/v0.1.17) (2013-11-13) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.16...v0.1.17) + +**Closed issues:** + +- Problem with weibo backend [\#85](https://github.com/omab/python-social-auth/issues/85) +- Steam auth 401 Client Error: Unauthorized [\#82](https://github.com/omab/python-social-auth/issues/82) +- Unit Test Django Client Login? [\#81](https://github.com/omab/python-social-auth/issues/81) +- Facebook profile picture [\#80](https://github.com/omab/python-social-auth/issues/80) +- AttributeError at /user/login/yahoo/ 'Association' object has no attribute 'id' [\#78](https://github.com/omab/python-social-auth/issues/78) +- Extending mongoengine User model for SOCIAL\_AUTH\_USER\_MODEL [\#70](https://github.com/omab/python-social-auth/issues/70) +- Clarify what the callback url should be for github backend [\#66](https://github.com/omab/python-social-auth/issues/66) +- Duplicate entry error when updating an existing user with a social user [\#63](https://github.com/omab/python-social-auth/issues/63) +- Problem with do\_complete for Facebook backend [\#39](https://github.com/omab/python-social-auth/issues/39) +- Using UserSocialAuth model with Django's generic FKs breaks [\#38](https://github.com/omab/python-social-auth/issues/38) + +**Merged pull requests:** + +- Use strategy.backend.name instead of strategy.backend\_name [\#88](https://github.com/omab/python-social-auth/pull/88) ([nitishr](https://github.com/nitishr)) +- Use strategy.backend.name instead of strategy.backend\_name [\#87](https://github.com/omab/python-social-auth/pull/87) ([nitishr](https://github.com/nitishr)) +- Use strategy.backend.name instead of strategy.backend\_name [\#86](https://github.com/omab/python-social-auth/pull/86) ([nitishr](https://github.com/nitishr)) +- Raise Http404 in django auth view when the backend is not found [\#83](https://github.com/omab/python-social-auth/pull/83) ([despawnerer](https://github.com/despawnerer)) +- Mod: URL for registering Windows Live key/secret [\#79](https://github.com/omab/python-social-auth/pull/79) ([yegle](https://github.com/yegle)) + +## [v0.1.16](https://github.com/omab/python-social-auth/tree/v0.1.16) (2013-11-07) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.15...v0.1.16) + +**Closed issues:** + +- TypeError at /auth/login/google/: int\(\) argument must be a string or a number, not Association [\#76](https://github.com/omab/python-social-auth/issues/76) +- Problem with Douban backend [\#72](https://github.com/omab/python-social-auth/issues/72) + +**Merged pull requests:** + +- Include actions module in distribution [\#77](https://github.com/omab/python-social-auth/pull/77) ([nijel](https://github.com/nijel)) +- Update partial from session with more recent values from kwargs [\#75](https://github.com/omab/python-social-auth/pull/75) ([branden](https://github.com/branden)) +- Tox support [\#74](https://github.com/omab/python-social-auth/pull/74) ([noirbizarre](https://github.com/noirbizarre)) +- quote message for url inclusion in Django middleware [\#73](https://github.com/omab/python-social-auth/pull/73) ([noirbizarre](https://github.com/noirbizarre)) +- Return the updated dict. [\#71](https://github.com/omab/python-social-auth/pull/71) ([branden](https://github.com/branden)) + +## [v0.1.15](https://github.com/omab/python-social-auth/tree/v0.1.15) (2013-11-04) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.14...v0.1.15) + +**Closed issues:** + +- Complete authentication through REST API [\#68](https://github.com/omab/python-social-auth/issues/68) +- Is there a short way to connect a social account to existing user [\#62](https://github.com/omab/python-social-auth/issues/62) +- typo in docstring [\#61](https://github.com/omab/python-social-auth/issues/61) +- Latest version tag gone wrong [\#60](https://github.com/omab/python-social-auth/issues/60) +- LinkedIn extra\_data only partially retrieved [\#57](https://github.com/omab/python-social-auth/issues/57) +- Django/Facebook login issue [\#56](https://github.com/omab/python-social-auth/issues/56) +- user\_details pipeline does not update protected fields for new users [\#55](https://github.com/omab/python-social-auth/issues/55) +- Bug in login with Django 1.6 [\#53](https://github.com/omab/python-social-auth/issues/53) +- Token refreshing [\#52](https://github.com/omab/python-social-auth/issues/52) +- Simple question - template use [\#50](https://github.com/omab/python-social-auth/issues/50) +- Django - Error when I try to run ./manage.py [\#48](https://github.com/omab/python-social-auth/issues/48) + +**Merged pull requests:** + +- Add Tornado Support. [\#69](https://github.com/omab/python-social-auth/pull/69) ([san-mate](https://github.com/san-mate)) +- Function user\_data returns list. This leads to exception in social/backe... [\#67](https://github.com/omab/python-social-auth/pull/67) ([akamit](https://github.com/akamit)) +- Add RunKeeper [\#65](https://github.com/omab/python-social-auth/pull/65) ([JasonSanford](https://github.com/JasonSanford)) +- Make partial\_pipeline JSON serializable for django 1.6 [\#64](https://github.com/omab/python-social-auth/pull/64) ([hannseman](https://github.com/hannseman)) +- Appsfuel doc from dsa to psa [\#59](https://github.com/omab/python-social-auth/pull/59) ([z4r](https://github.com/z4r)) +- Add openSUSE OpenID login [\#51](https://github.com/omab/python-social-auth/pull/51) ([nijel](https://github.com/nijel)) +- `sanitize\_redirect` don't work with Django's `reverse\_lazy` [\#49](https://github.com/omab/python-social-auth/pull/49) ([volrath](https://github.com/volrath)) + +## [v0.1.14](https://github.com/omab/python-social-auth/tree/v0.1.14) (2013-10-07) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.13...v0.1.14) + +**Closed issues:** + +- Amazon oauth , client\_id of None in url? [\#47](https://github.com/omab/python-social-auth/issues/47) +- AttributeError: 'str' object has no attribute '\_meta' in Django's admin.py [\#45](https://github.com/omab/python-social-auth/issues/45) +- Invalid documentation for Yahoo OAuth key/secret [\#43](https://github.com/omab/python-social-auth/issues/43) +- using mongoengine \> 0.8, referencefields now store objectids not dbrefs [\#42](https://github.com/omab/python-social-auth/issues/42) +- Google OAuth2 Disconnect [\#41](https://github.com/omab/python-social-auth/issues/41) +- KeyError at /complete/facebook/ when trying to sign in without verifying e-mail address [\#40](https://github.com/omab/python-social-auth/issues/40) +- MongoEngine compability [\#37](https://github.com/omab/python-social-auth/issues/37) +- TypeError at /complete/facebook/ [\#36](https://github.com/omab/python-social-auth/issues/36) + +**Merged pull requests:** + +- Fixes \#45 -- AttributeError while resolving the user model in Django [\#46](https://github.com/omab/python-social-auth/pull/46) ([MarkusH](https://github.com/MarkusH)) +- Add python 3.3 and django 1.6 compatibility [\#44](https://github.com/omab/python-social-auth/pull/44) ([nvbn](https://github.com/nvbn)) + +## [v0.1.13](https://github.com/omab/python-social-auth/tree/v0.1.13) (2013-09-22) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.12...v0.1.13) + +**Closed issues:** + +- Error: django.db.models.fields.subclassing.JSONField [\#35](https://github.com/omab/python-social-auth/issues/35) +- some linkedin oauth2 extra data doesn't show up [\#34](https://github.com/omab/python-social-auth/issues/34) +- Odnoklassniki backend requires authization by POST [\#33](https://github.com/omab/python-social-auth/issues/33) +- SOCIAL\_AUTH\_GOOGLE\_OAUTH2\_EXTRA\_SCOPE is ignored [\#28](https://github.com/omab/python-social-auth/issues/28) +- Example for pyramid [\#27](https://github.com/omab/python-social-auth/issues/27) +- Not working with instagram api [\#21](https://github.com/omab/python-social-auth/issues/21) + +**Merged pull requests:** + +- Update README.rst [\#32](https://github.com/omab/python-social-auth/pull/32) ([jontsai](https://github.com/jontsai)) +- Update pipeline.rst [\#31](https://github.com/omab/python-social-auth/pull/31) ([jontsai](https://github.com/jontsai)) +- Update README.rst [\#30](https://github.com/omab/python-social-auth/pull/30) ([jontsai](https://github.com/jontsai)) + +## [v0.1.12](https://github.com/omab/python-social-auth/tree/v0.1.12) (2013-09-13) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.11...v0.1.12) + +**Closed issues:** + +- Setting the facebook scope wrongly documented [\#29](https://github.com/omab/python-social-auth/issues/29) + +**Merged pull requests:** + +- Fixed auth redirect URL for BaseOauth2 always redirecting wrong [\#26](https://github.com/omab/python-social-auth/pull/26) ([romanalexander](https://github.com/romanalexander)) +- Adding support for ThisIsMyJam [\#25](https://github.com/omab/python-social-auth/pull/25) ([systemizer](https://github.com/systemizer)) +- Add support for box.net [\#24](https://github.com/omab/python-social-auth/pull/24) ([samkuehn](https://github.com/samkuehn)) + +## [v0.1.11](https://github.com/omab/python-social-auth/tree/v0.1.11) (2013-09-04) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.10...v0.1.11) + +**Closed issues:** + +- Steam user ID broken in Django backend [\#23](https://github.com/omab/python-social-auth/issues/23) +- Flask example fails to complete connection to Github [\#22](https://github.com/omab/python-social-auth/issues/22) + +## [v0.1.10](https://github.com/omab/python-social-auth/tree/v0.1.10) (2013-08-29) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.9...v0.1.10) + +## [v0.1.9](https://github.com/omab/python-social-auth/tree/v0.1.9) (2013-08-29) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.8...v0.1.9) + +**Closed issues:** + +- google oauth 2.0 [\#20](https://github.com/omab/python-social-auth/issues/20) +- Support for linkedin oauth2 [\#19](https://github.com/omab/python-social-auth/issues/19) +- Invalid Steam backend user id. [\#17](https://github.com/omab/python-social-auth/issues/17) + +**Merged pull requests:** + +- SQLAlchemy fixes [\#18](https://github.com/omab/python-social-auth/pull/18) ([Flyflo](https://github.com/Flyflo)) + +## [v0.1.8](https://github.com/omab/python-social-auth/tree/v0.1.8) (2013-07-13) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.7...v0.1.8) + +**Merged pull requests:** + +- Fix OpenId auth with Flask 0.10 [\#16](https://github.com/omab/python-social-auth/pull/16) ([Flyflo](https://github.com/Flyflo)) +- Add CodersClan button [\#13](https://github.com/omab/python-social-auth/pull/13) ([Orchestrator81](https://github.com/Orchestrator81)) +- Added a default to response in FacebookOAuth.do\_auth [\#12](https://github.com/omab/python-social-auth/pull/12) ([san-mate](https://github.com/san-mate)) +- Bug fix of FacebookAppOAuth2 [\#11](https://github.com/omab/python-social-auth/pull/11) ([san-mate](https://github.com/san-mate)) + +## [v0.1.7](https://github.com/omab/python-social-auth/tree/v0.1.7) (2013-06-03) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.6...v0.1.7) + +## [v0.1.6](https://github.com/omab/python-social-auth/tree/v0.1.6) (2013-06-03) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.5...v0.1.6) + +## [v0.1.5](https://github.com/omab/python-social-auth/tree/v0.1.5) (2013-06-01) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.4...v0.1.5) + +## [v0.1.4](https://github.com/omab/python-social-auth/tree/v0.1.4) (2013-05-31) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.3...v0.1.4) + +## [v0.1.3](https://github.com/omab/python-social-auth/tree/v0.1.3) (2013-05-31) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.2...v0.1.3) + +**Closed issues:** + +- get\_user\_details\(\) vs user\_data\(\) [\#7](https://github.com/omab/python-social-auth/issues/7) + +**Merged pull requests:** + +- Added support for django custom user with no 'username' field [\#10](https://github.com/omab/python-social-auth/pull/10) ([jgsogo](https://github.com/jgsogo)) +- Add Trello backend support [\#9](https://github.com/omab/python-social-auth/pull/9) ([dongweiming](https://github.com/dongweiming)) +- Podio backend [\#8](https://github.com/omab/python-social-auth/pull/8) ([gsakkis](https://github.com/gsakkis)) +- VK.com \(former vkontakte\) backend update [\#6](https://github.com/omab/python-social-auth/pull/6) ([uruz](https://github.com/uruz)) +- Bug fix with Vkontakte provider [\#5](https://github.com/omab/python-social-auth/pull/5) ([kazarinov](https://github.com/kazarinov)) + +## [v0.1.2](https://github.com/omab/python-social-auth/tree/v0.1.2) (2013-04-04) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.1.1...v0.1.2) + +**Closed issues:** + +- Flask example - missing relation 'social\_auth\_usersocialauth' [\#4](https://github.com/omab/python-social-auth/issues/4) + +## [v0.1.1](https://github.com/omab/python-social-auth/tree/v0.1.1) (2013-04-01) +**Closed issues:** + +- confusing update to globals in the flask integration [\#1](https://github.com/omab/python-social-auth/issues/1) + +**Merged pull requests:** + +- Fixed South introspection path to new module structure. [\#2](https://github.com/omab/python-social-auth/pull/2) ([jezdez](https://github.com/jezdez)) + + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/Changelog b/Changelog deleted file mode 100644 index abe8ab835..000000000 --- a/Changelog +++ /dev/null @@ -1,4213 +0,0 @@ -2016-01-14 HEAD (unreleased) -============================ - - * 2016-01-14 Matías Aguirre - PEP8 - - * 2015-12-23 Matías Aguirre - Document run_tox script dependencies - - * 2015-12-23 Matías Aguirre - PEP8 and doc styles applied - - * 2015-12-16 Yuri Prezument - Fix Django 1.10 deprecation warnings - - * 2015-12-08 Matías Aguirre - Typos and PEP8 - - * 2015-12-05 Romulo Tavares - Changed instagram backend to new authorization routes - - * 2015-12-01 James Keys - Update settings.rst - - * 2015-11-23 Matías Aguirre - Add instagram authentication backend to docs - - * 2015-11-17 SeokJun Hong - fix Travis-CI failed - - * 2015-11-16 SeokJun Hong - fix a typing error in naver.rst - - * 2015-11-16 SeokJun Hong - Add naver backends - - * 2015-11-11 Matthew Jones - Formatter fixes for SAML to support Py2.6 - - * 2015-10-20 Martín Prunell - Fix typo - - * 2015-10-17 lneoe - use `self.setting('USE_OPENID_AS_USERNAME', False)` this will fast - - * 2015-10-14 lneoe - use `openid` as username - - * 2015-10-16 Kevin Harvey - Fixes a few grammar issues in the docs - - * 2015-10-12 Peter Schmidt - Fix a few typos in backends - - * 2015-10-07 Sergey Trofimov - Update odnoklassniki.py - - * 2015-10-07 Sergey Trofimov - Update vk.py - - * 2015-10-05 Matías Aguirre - Switch import order - - * 2015-10-05 Matías Aguirre - Link justgiving doc - - * 2015-09-16 Matías Aguirre - Fix missing slash - - * 2015-09-16 Matías Aguirre - Fix kwarg - - * 2015-09-16 Matías Aguirre - Fix typo - - * 2015-09-16 Matías Aguirre - Add signed request signature for Instagram - - * 2015-10-05 Matías Aguirre - Codestyle and missing docs - - * 2015-10-05 Matías Aguirre - Fix syntax error - - * 2015-10-05 Matías Aguirre - Remove python3.5 (openid breaks), silence pypy warings - - * 2015-10-05 Matías Aguirre - Remove duplicated pypy entry, add python 3.5 - - * 2015-10-05 Matías Aguirre - Pypy test requirements for travisci - - * 2015-10-05 Matías Aguirre - Fix pypy support, no python-saml for it, silence python2.6 warnings - - * 2015-10-04 Matías Aguirre - Fix travis conf - - * 2015-10-04 Matías Aguirre - Syntax typo - - * 2015-10-04 Matías Aguirre - Swtich travisci from legacy to container infrastructure - - * 2015-10-02 Maarten van Schaik - Store all tokens on refresh - - * 2015-09-29 Maarten van Schaik - Save extra_data on login - - * 2015-09-28 Luke Briner - Update URLs to match new site and remove OAuth comment. - -2015-09-25 v0.2.13 -================== - - * 2015-09-25 Matías Aguirre - v0.2.13 - - * 2015-09-23 James Maddox - added Python 3 support to FacebookAppOAuth2's load_signed_request method - - * 2015-09-15 alrusdi - VK API workflow fix if error happens on vk-side - - * 2015-09-07 Bulgantamir Gankhuyag - added AuthUnreachableProvider exception to documentation - - * 2015-09-02 Michael Willmott - Added justgiving.com OAuth2 backend - - * 2015-09-01 Julian Bez - Add REDIRECT_STATE = False - - * 2015-08-31 Matías Aguirre - Remove template tag - - * 2015-08-31 Matías Aguirre - Fix Google+ auth complete, update examples with updated SDK usage. Refs - #316 - - * 2015-08-24 Andy - Fix typo in pipeline doc - - * 2015-08-20 Michał Zagdan - Update facebook.rst - - * 2015-08-19 Jerzy Spendel - One more coma in use_cases.rst - - * 2015-08-18 Jerzy Spendel - Coma at the end of every tuple and dict - - * 2015-08-18 Jerzy Spendel - Tuple in pipeline's documentation should be ended with coma - - * 2015-08-15 Chun-Jung Lee - Support Pyramid Authentication Policies - - * 2015-08-14 Ajoy Oommen - Fix another mistake in use_cases.rst - - * 2015-08-14 Ajoy Oommen - Fix typo in use_cases.rst - - * 2015-08-14 Matías Aguirre - Small cosmetic changes and link from main index. Refs #700 - - * 2015-08-14 Matías Aguirre - Small cosmetic changes and link from main index. Refs #700 - - * 2015-08-10 Henoc Díaz - References to UberOAuth2 backend docs added in intro.rst and - backends/index.rst - - * 2015-08-10 Henoc Díaz - Add support for Uber OAuth2 - Uber API v1 - Backend UberOAuth2 added - - Tests for UberOAuth2 backend added - Docs for backend added - - * 2015-08-07 Matías Aguirre - Fix flask remember-me functionality - - * 2015-08-07 Matías Aguirre - Retrieve remember value from session - - * 2015-08-05 khamaileon - Fix #703 - - * 2015-07-31 Chris Curvey - formatting cleanups - - * 2015-07-31 Chris Curvey - more formatting - - * 2015-07-31 Chris Curvey - formatting test - - * 2015-07-31 Chris Curvey - first revision - - * 2015-07-29 Troy Grosfield - Adding abstract UserSocialAuth model as well as a model manager class. - - * 2015-07-29 Troy Grosfield - Adding a model manager for the django app. - - * 2015-07-25 James Little - Update main.py - - * 2015-07-22 Matías Aguirre - Link docs and PEP8 - - * 2015-07-21 Can Kaya - removed @app.teardown_request since it is called before - @app.teardown_appcontext and removes current session. With - @app.teardown_request session is removed just before session.commit and - logged in user can not be saved on db. - - * 2015-07-21 Georgy Cheshkov - Remove debug printing from BaseOAuth2 backend - - * 2015-07-19 João Miguel Neves - support for goclio.eu service - - * 2015-07-16 Jordan Reiter - text -> content solves "is not JSON serializable" - - * 2015-07-16 Frankie Robertson - Close #622 by explicitly setting email length (compatibility with Django - 1.7) - - * 2015-07-16 Lee Jaeyoung - Fix test failed: get right user_id from response - - * 2015-07-16 Lee Jaeyoung - Add orbi backend - - * 2015-07-15 Matías Aguirre - Meetup PEP8 and link docs - - * 2015-07-14 Jesse Pollak - fix clef backend by access ID in correct way - - * 2015-07-13 bluszcz - Clean up - - * 2015-07-13 bluszcz - Fixed typo - - * 2015-07-13 bluszcz - Added Meetup.com OAuth2 backend - - * 2015-07-10 paxapy - echosign OAuth2 backend - -2015-07-10 v0.2.12 -================== - - * 2015-07-10 Matías Aguirre - v0.2.12 - - * 2015-07-10 Julian Bez - Use full - - * 2015-07-10 Julian Bez - Use key - - * 2015-07-10 Julian Bez - Fix 'QueryDict' object has no attribute 'dicts' - - * 2015-07-10 Maarten van Schaik - Fix redirect_uri issue with tornado reversed url - - * 2015-07-09 Matías Aguirre - Fix doc title in withins file - - * 2015-07-09 Matías Aguirre - PEP8 and code simplification - - * 2015-07-09 eshellman - Update settings.rst - - * 2015-07-09 Matías Aguirre - Backward compatibility option. Refs #652 - - * 2015-07-09 Matías Aguirre - Add comment refering to ticket. Refs #671, #672 - - * 2015-07-09 Maksim Sokolskiy - fix(pipeline): fix user_detail pipeline issue - - * 2015-07-03 Maarten van Schaik - Fix decoding of request args - - * 2015-07-03 Maarten van Schaik - Fix cookie handling for tornado - - * 2015-06-28 Igor Serko - added support for Github Enterprise - - * 2015-06-25 Tomas - Withings Backend - - * 2015-06-24 Braden MacDonald - Use official python-saml 2.1.3 release, remove setting that is no longer - included - - * 2015-06-24 Braden MacDonald - Fix wrong placement of changelog commits in 76a27b2 - -2015-06-24 v0.2.11 -================== - - * 2015-06-24 Matías Aguirre - v0.2.11 - - * 2015-06-21 Mark Adams - Added an OAuth2 backend for Bitbucket - - * 2015-06-21 Mark Adams - Updated Bitbucket backend to use the UUID as the ID_KEY - - * 2015-06-21 Mark Adams - Updated Bitbucket backend to use newer 2.0 APIs instead of 1.0 - - * 2015-06-19 Matías Aguirre - Link docs - - * 2015-06-19 Matías Aguirre - PEP8 - - * 2015-06-18 Braden MacDonald - Added documentation - - * 2015-06-17 Maarten van Schaik - Python3 fixes for Tornado - - * 2015-06-17 Matías Aguirre - PEP8 - - * 2015-06-01 Aurélien Bompard - Keep the egg-info directory in the sdist - -2015-05-29 v0.2.10 -================== - - * 2015-05-29 Matías Aguirre - v0.2.10 - - * 2015-05-29 Matías Aguirre - Newline at end of file - - * 2015-05-29 Matías Aguirre - Fix changetip docs errors - - * 2015-05-29 Matías Aguirre - Coding style - - * 2015-05-29 Matías Aguirre - PEP8 - - * 2015-05-29 Matías Aguirre - PEP8 - - * 2015-05-29 Matías Aguirre - Avoid storing empty values from user details - - * 2015-05-26 vinhub - Added Azure AD docs to index.rst - - * 2015-05-26 Vinayak (Vin) Bhalerao - Removed - - * 2015-05-25 sushantgawali - changes in extra_data - - * 2015-05-18 sushantgawali - added copyright / license notices - - * 2015-05-15 sushantgawali - updated test cases - - * 2015-05-13 sushantgawali - added get_auth_token method for ease of use - - * 2015-05-12 sushantgawali - added test cases for AzureADOAuth2 refresh token method added - - * 2015-05-11 sushantgawali - added generic resource setting added license text added documentation file - - * 2015-05-11 sushantgawali - cleaned up unneeded Sharepoint related code - - * 2015-05-11 sushantgawali - Added provider for Microsoft Azure Active Directory OAuth2 - - * 2015-05-21 Braden MacDonald - Minor cleanups - - * 2015-05-20 Braden MacDonald - Minor consistency fix - - * 2015-05-20 Braden MacDonald - Add an integration point for extra security layers like - eduPersonEntitlement - - * 2015-05-20 Braden MacDonald - Make IdP name format slightly more flexible - - * 2015-05-20 Marek Jalovec - Fixes "ImportError: No module named packages.urllib3.poolmanager" error - (fixes #617) - - * 2015-05-20 blurrcat - fix Fitbit OAuth 1 authorization URL - - * 2015-05-17 duoduo369 - add weixin backends - - * 2015-05-16 Andrew Starr-Bochicchio - Add a DigitalOcean backend. - - * 2015-05-11 Braden MacDonald - Add python-saml requirement (temporary commit) - - * 2015-05-08 Braden MacDonald - Tests for SAML backend - - * 2015-04-30 Braden MacDonald - SAML2 backend using OneLogin's python-saml - - * 2015-05-08 Matías Aguirre - Ensure that all the requirements are installed - - * 2015-05-08 Matías Aguirre - Fix syntax (backward compatible) - - * 2015-05-07 Matías Aguirre - Dev version flagged - -2015-05-07 v0.2.9 -================= - - * 2015-05-07 Matías Aguirre - v0.2.9 - - * 2015-05-07 Matías Aguirre - Fix manifest definition - -2015-05-07 v0.2.8 -================= - - * 2015-05-07 Matías Aguirre - v0.2.8 - - * 2015-05-02 Avi אבי Alkalay אלקלעי - Fixed Moves link on list of providers - - * 2015-05-02 Avi אבי Alkalay אלקלעי - Added Moves to the list of providers - - * 2015-05-01 Matías Aguirre - Add note about TLSv1 support in Amazon backend - - * 2015-05-01 Matías Aguirre - Support SSL protocol override, default Amazon to TLSv1. Fixes #603 - - * 2015-04-27 Matías Aguirre - Fix typos in docs (thanks to vsobolmaven) - - * 2015-04-21 Matías Aguirre - Make URLs trailing slash be configurable by setting. Refs #505 - - * 2015-04-20 Matías Aguirre - Use get_json() helper - - * 2015-04-20 Nick Sullivan - Documentation for ChangeTip backend - - * 2015-04-20 Nick Sullivan - fail gracefully on missing email - - * 2015-04-19 Matías Aguirre - Allow to remove team from username in slack backend - - * 2015-04-19 Matías Aguirre - Flag dev version - -2015-04-19 v0.2.7 -================= - - * 2015-04-19 Matías Aguirre - v0.2.7 - - * 2015-04-19 Matías Aguirre - Don't send redirect_state to slack backend - - * 2015-04-17 Nick Sullivan - use api domain for changetip request - - * 2015-04-17 Nick Sullivan - ChangeTip backend - - * 2015-04-16 Matías Aguirre - Fix clean username regex. Fixes #594 - - * 2015-04-16 Matías Aguirre - PEP8 and switch check order - - * 2015-04-16 Matías Aguirre - Take into account that sometimes API v2.3 returns the old querystring - format. Fixes #592 - - * 2015-04-16 Matías Aguirre - Move OAuth1 method out from the base class - - * 2015-04-16 zz - Fix the final_username may be empty and will skip the loop. - - * 2015-04-16 ys.chi - Alter email max length for Django app - - * 2015-04-15 Matías Aguirre - Remove single-use var - - * 2015-04-15 Matías Aguirre - Clean any pipeline remanents when starting the process. Refs #325 - - * 2015-04-15 Matías Aguirre - Flag dev version - - * 2015-04-15 Matías Aguirre - Swtich Twitter API to POST (as it's documented) - - * 2015-04-15 Christian Pedersen - Append trailing slash in Django - -2015-04-14 v0.2.6 -================= - - * 2015-04-14 Matías Aguirre - v0.2.6 - - * 2015-04-14 Matías Aguirre - Include tests requirements files. Fixes #590 - - * 2015-04-13 Matías Aguirre - Fix publish task - -2015-04-13 v0.2.5 -================= - - * 2015-04-13 Matías Aguirre - v0.2.5 - - * 2015-04-13 Matías Aguirre - Fix wheel support. Refs #588 - - * 2015-04-13 Matías Aguirre - Add email to default list of protected userfields (popular demand) - -2015-04-11 v0.2.4 -================= - - * 2015-04-11 Matías Aguirre - v0.2.4 - - * 2015-04-11 Matías Aguirre - Fix setting name (make it backend related). Refs #586 - - * 2015-04-10 Matías Aguirre - Link to post about access-token based authentication - - * 2015-04-08 Matías Aguirre - Move revoke methods to common class. Fixes #484 - - * 2015-04-08 Matías Aguirre - Fix settings names on spotify docs. Fixes #475 - - * 2015-04-07 Matías Aguirre - Fix links in docs - - * 2015-04-07 Matías Aguirre - Fix Facebook test case after API version change. Refs #480 - - * 2015-04-07 Matías Aguirre - Update Facebook to API v2.3 - - * 2015-04-07 Matías Aguirre - Fix get_scope() override example - - * 2015-04-07 Matías Aguirre - Raise error if token was passed but it's incomplete. Fixes #574 - - * 2015-04-06 Matías Aguirre - Flag dev version - - * 2015-04-06 Matt Robenolt - Build a wheel, and upload with twine - - * 2015-04-04 Matías Aguirre - Log error messages. Fixes #507 - - * 2015-04-04 Matías Aguirre - Pass all arguments to extra_data (save access token). - - * 2015-04-04 Matías Aguirre - Improve http error handling on auth_complete/do_auth. Fixes #304 - - * 2015-04-04 Matías Aguirre - Update docs about SOCIAL_AUTH_PROTECTED_USER_FIELDS. Fixes #459 - - * 2015-04-04 Matías Aguirre - Optional trailing slash on django apps. Fixes #505 - - * 2015-04-04 Matías Aguirre - Improve deprecation notice on behance docs - - * 2015-04-04 Matías Aguirre - Add notice about behance broken api. Refs #530 - - * 2015-04-04 Matías Aguirre - Remove unsupported attribute from alter field migration - - * 2015-04-04 Matías Aguirre - Remove hard limitations on PyJWT and requests-oauthlib versions. Fixes #531 - - * 2015-04-04 Matías Aguirre - Change title - - * 2015-04-04 Matías Aguirre - Link backend docs - - * 2015-04-04 Matías Aguirre - Add docs about disconnection and logging out difference. Fixes #568 - - * 2015-04-03 Matías Aguirre - Conditional import on transaction, update docs to mention it. Fixes #572 - - * 2015-04-03 Matías Aguirre - Define a MANIFEST.in file. Fixes #578 - - * 2015-04-03 Lucas Roesler - Allow inactive users to login - - * 2015-04-02 Matías Aguirre - Update django/mongoengine example (similar to default one). Refs #576 - - * 2015-04-02 Matías Aguirre - Add backward compatibility on django app initialization. Refs #550 - - * 2015-04-01 M.Yasoob Ullah Khalid ☺ - Update LICENSE - -2015-03-31 v0.2.3 -================= - - * 2015-03-31 Matías Aguirre - v0.2.3 - - * 2015-03-31 Matías Aguirre - PEP8. Refs #570 - - * 2015-03-30 Krzysztof Hoffmann - Added NaszaKlasa OAuth2 support - - * 2015-03-29 Buddy Lindsey, Jr. - Add revoke token ability to strava - - * 2015-03-29 Matías Aguirre - Store github login in extra data by default. Refs #567 - - * 2015-03-25 Jun Wang - set redirect_state to false for live oauth2 - - * 2015-03-25 Matías Aguirre - Fix backend, add quick docs. Refs #549 - - * 2015-03-25 Matías Aguirre - Add rednose to python3 requirements too - - * 2015-03-23 Jerome Lefeuvre - Add setup.cfg to configure flake8 and nosetests - - * 2015-03-23 Jerome Lefeuvre - Add rednose for colored output log - - * 2015-03-21 Andrei Petre - Add missing migration for Django app - - * 2015-03-19 José Padilla - Specify algorithm for encoding and decoding - - * 2015-03-19 José Padilla - Require PyJWT>=1.0.0,<2.0.0 - - * 2015-03-19 Matías Aguirre - Remove debug print - - * 2015-03-19 Matías Aguirre - Flush sqlalchemy session to get the object ids. Refs #390 - - * 2015-03-19 Johannes - Start pipeline with default details arg - - * 2015-03-19 Jerome Lefeuvre - Add `python_chameleon` to setup - - * 2015-03-17 Matías Aguirre - Ensure to flush the db session (needed for Pyramid + sqlalchemy). Refs #390 - - * 2015-03-12 DanielJDufour - update for django 1.9 - - * 2015-03-12 Matt Howland - Create vend.py - - * 2015-03-12 Johannes - Increase min request-oauthlib version to 0.3.1 - - * 2015-03-12 Adam Bogdał - Add wunderlist backend to the list - - * 2015-03-11 Florian Eßer - Update index.html - - * 2015-03-10 Adam Bogdał - Add wunderlist oauth2 backend - - * 2015-03-07 Matías Aguirre - PEP8, quotes and extra_data - - * 2015-03-06 Florian Eßer - Add backend for EVE Online Single Sign-On (OAuth2) - https://developers.eveonline.com/resource/single-sign-on - - * 2015-03-05 Matías Aguirre - PEP8 and simplify code - - * 2015-03-05 Matías Aguirre - PEP8 - - * 2015-03-05 Rafael Muñoz Cárdenas - Add extra info on Google+ Sign-In doc - - * 2015-03-03 dobestan - update Kakao OAuth2 backend : update auth process- Fixes #538 - - * 2015-03-03 dobestan - Enable KakaoOAuth2 on example app - - * 2015-03-03 dobestan - Disable redirect_state in kakao backend. Fixes #538 - - * 2015-03-02 Tom Clancy - Update google.rst - - * 2015-03-02 Hassek - modified docs - - * 2015-02-25 Hassek - fixed refresh tokens for yahoo - - * 2015-02-25 Hassek - added OAuth2 support to yahoo. Also, removed OAuth1 since yahoo will not be - supporting it anymore - - * 2015-02-24 Matías Aguirre - Cleanup imports and hmac creation, fix python3 compatibility - - * 2015-02-24 zz - Fix Issue #532, get UID when use access_token ajax auth in weibo backends. - - * 2015-02-24 zz - Fix Issue #532, get UID when use access_token ajax auth in weibo backends. - - * 2015-02-23 Matías Aguirre - Fix zotero tests - -2015-02-23 v0.2.2 -================= - - * 2015-02-23 Matías Aguirre - v0.2.2 - - * 2015-02-23 Matías Aguirre - PEP8/PyFlakes - - * 2015-02-21 Motoki Naruse - login_user takes 3 parameters - - * 2015-02-21 Motoki Naruse - Include template engine - - * 2015-02-21 Motoki Naruse - Email column is duplicated - - * 2015-02-18 Sergey Kozub - fix python3 handling of openid backend on sqlalchemy storage (use str - instead of bytes) - - * 2015-02-16 Chris Lamb - Don't use "import" in example method paths docs to avoid confusion - - * 2015-02-15 tell-k - fixed bug. - - * 2015-02-15 tell-k - add document url. - - * 2015-02-15 tell-k - Add dribble backend. - - * 2015-02-14 Alejandro Baronetti - Fixed issue: GET dictionary is immutable. I am not using MergeDict because - it will be deprecated - - * 2015-02-13 Chris Martin - Include username in Reddit extra_data - - * 2015-02-12 Eugene Agafonov - [facebook-oauth2] Verifying Graph API Calls with appsecret_proof - - * 2015-02-11 tell-k - refs #512 fixed typo - - * 2015-02-11 tell-k - refs #512 fixed bug for py2.6 - - * 2015-02-11 tell-k - add qiita backend - - * 2015-02-10 Matías Aguirre - Pyflakes - - * 2015-02-10 Matías Aguirre - PEP8 - - * 2015-02-07 Alejandro Baronetti - Fix: REQUEST has been deprecated in Django 1.7, so we need to merge - dictionaries - - * 2015-02-06 Clinton Blackburn - Updated PyJWT Dependency - - * 2015-02-06 Rafael Muñoz Cárdenas - Update Google documentation - - * 2015-02-03 Matías Aguirre - Add test for nationbuilder backend - - * 2015-02-03 Matías Aguirre - Move common code to base class - - * 2015-02-03 Matías Aguirre - NationBuilder backend - - * 2015-02-03 Matías Aguirre - Define methods to customize urls in OAuth2 backends - - * 2015-02-03 Matías Aguirre - Enable debug pipeline in example app - - * 2015-02-03 Ian Wienand - Ensure email is not None - - * 2015-02-02 Chris DeBlois - updated mendeley oauth2 to use new api resource and also updated to grab - new profile_id, name and bio - - * 2015-02-02 Ian Wienand - Add support for Launchpad OpenId - - * 2015-01-30 rivf - Fixed jawbone authentification - - * 2015-01-27 Adam Babik - Added coursera backend to README - - * 2015-01-27 Adam Babik - Docs for coursera backend - - * 2015-01-23 Adam Babik - Added Coursera backend to django_example - - * 2015-01-23 Adam Babik - Added backend for Coursera - - * 2015-01-20 ayush - Added nonce unique constraint - - * 2015-01-19 Matías Aguirre - Patch tornado arguments/cookies getting. Refs #445. Refs #346 - - * 2015-01-10 Chris Barna - Store Spotify's refresh_token. - - * 2015-01-07 Nick Sullivan - cleanly handle both a scope of 'identity' only and also fill in more data - if we have 'read' access - - * 2015-01-07 Nick Sullivan - properly handle data, so that it is more future proof, again. This time fix - issue with team_url - - * 2015-01-07 Nick Sullivan - update in a way that will be more future proof - - * 2015-01-07 Nick Sullivan - when scope is reduced, the response from slack is different, handle both - - * 2015-01-05 Ben Davis - Fixed extra_data field in django 1.7 initial migration - - * 2015-01-02 Jun Wang - Fix YahooOAuth get primary email sorting order - - * 2015-01-02 Matías Aguirre - PEP8 - - * 2015-01-02 Matías Aguirre - Link docs, apply PEP8, change quotes and code style - - * 2015-01-02 Matías Aguirre - Change expression - - * 2015-01-02 travoltino - Update base.py - - * 2014-12-29 Alex Muller - Correct Django SESSION_COOKIE_AGE setting - - * 2014-12-28 Nick Sullivan - Documentation for slack backend - - * 2014-12-28 Nick Sullivan - Slack backend - - * 2014-12-24 Alex Muller - Update GitHub documentation - - * 2014-12-19 Frankie Robertson - Fix #460: Call force_text on _URL settings to support reverse_lazy with - default session serializer - - * 2014-11-27 James Potter - Update django.rst - - * 2014-11-26 Anna Warzecha - User ID is required to use any further requests - - * 2014-11-26 Sasha Golubev - Added backend for professionali.ru - - * 2014-11-24 Lukas Klein - Removed Orkut backend - - * 2014-11-24 Matías Aguirre - Remove Flask-SQLAlchemy dependency from example app - - * 2014-11-23 Matías Aguirre - Simplify flask app initialization - - * 2014-11-22 Matías Aguirre - Allow initial definition of protected attributes - - * 2014-11-22 Matías Aguirre - Quick khan academy docs - - * 2014-11-22 Matías Aguirre - PEP8 - - * 2014-11-22 Matías Aguirre - PEP8 - - * 2014-11-21 tschilling - Allow the pipeline to change the redirect url. Moves the popping of the - redirect value from the session to after the pipe line executes. - - * 2014-11-19 Seán Hayes - Added support for Django's User.EMAIL_FIELD. - - * 2014-11-18 Anna Warzecha - Changed test name - - * 2014-11-18 Anna Warzecha - Fixed docs - - * 2014-11-18 Anna Warzecha - Khan Academy oauth support now fully working - - * 2014-11-18 Anna Warzecha - Struggling with Khan Academy again... - - * 2014-11-16 Anna Warzecha - Fix backend name formatting - - * 2014-11-16 Anna Warzecha - Fix readme - - * 2014-11-16 Anna Warzecha - Basic Khan Academy support - - * 2014-11-15 Matías Aguirre - Avoid override of custom-usernames fields with plain generated username. - Refs #435 - - * 2014-11-15 Matías Aguirre - Fix docs. Refs #436 - - * 2014-11-14 Matías Aguirre - Remove x flag from .py file - - * 2014-11-11 Matías Aguirre - Example on how to re-prompt a google user to get the refresh_token - - * 2014-11-07 Miguel Paolino - Added zotero test, work in progress - - * 2014-11-07 Miguel Paolino - Udpated README to include the Zotero backend mention - - * 2014-11-07 Miguel Paolino - Fixed doc line - - * 2014-11-07 Miguel Paolino - Added Zotero OAuth1 backend - - * 2014-11-02 Matías Aguirre - Pass request to pyramid strategy. Refs #390 - - * 2014-11-02 Matías Aguirre - Update changelog. Refs #421 - - * 2014-11-01 Matías Aguirre - PEP8 - - * 2014-11-01 John Lynn - Fix typo for AUTH_USER_MODEL - - * 2014-11-01 Matías Aguirre - Set no-cache on views - - * 2014-11-01 Matías Aguirre - Rename tokens to access_token. Refs #430 - - * 2014-11-01 Matías Aguirre - PEP8 - - * 2014-11-01 Matías Aguirre - PEP8 + basic docs. Refs #412 - - * 2014-11-01 Matías Aguirre - PEP8 - - * 2014-10-30 Matías Aguirre - Link missing doc - - * 2014-10-30 Matías Aguirre - Fix use case snippet - - * 2014-10-29 Alex Parij - Update base.py - - * 2014-10-29 Mitchel Humpherys - use correct tense for `to meet' - - * 2014-10-26 John Lynn - Fix custom user model migrations for Django 1.7 - - * 2014-10-23 Matías Aguirre - Pick github primary email first. Fixes #413 - - * 2014-10-16 Christopher Grebs - Fix migration issue on python 3 - - * 2014-10-12 SilentSokolov - Fix does not match the number of arguments (for vk and ok backend) - - * 2014-10-08 Michal Karzyński - Salesforce OAuth2 support - - * 2014-10-07 Matías Aguirre - PEP8 and module rename - - * 2014-10-07 Matías Aguirre - Travisci update - - * 2014-10-07 Matías Aguirre - Enable pypy and replace sugar with unittest2. Refs #410 - - * 2014-10-07 Omer Katz - Added Python 3.4 and PyPy to the build matrix. - - * 2014-10-04 micahhausler - Added Django 1.7 App Config - - * 2014-10-02 micahhausler - Switched list_display order for UserSocialAuth - - * 2014-10-02 micahhausler - Added string method to UserSocialAuth model - - * 2014-10-03 Daniel Holmes - Use new GoogleOAuth2 Spec - - * 2014-10-01 Laban - Incorrect import path for db model - - * 2014-09-30 Matías Aguirre - PEP8 - - * 2014-09-30 Matías Aguirre - Convert docstring to comments, PEP8 - - * 2014-09-29 Lee Jaeyoung - Apply more detailed address for kakao - - * 2014-09-29 Lee Jaeyoung - Add Kakao to README.rst - - * 2014-09-28 David Zerrenner - Added some legal stuff - - * 2014-09-27 Aarni Koskela - Recreate migration with Django 1.7 final and re-PEP8. - - * 2014-09-26 Matías Aguirre - Use getattr to get current backend from request - - * 2014-09-25 Matías Aguirre - Doc about custom url namespace. Refs #399 - - * 2014-09-25 Matías Aguirre - Configurable django views namespace. Refs #399 - - * 2014-09-25 Matías Aguirre - PEP8 and more - - * 2014-09-24 Vera Mazhuga - master add SCOPE_SEPARATOR to DisqusOAuth2 - - * 2014-09-24 dzerrenner - added a backend for Battle.net Oauth2 auth - - * 2014-09-23 Matías Aguirre - PEP8 - - * 2014-09-23 Matías Aguirre - PEP8 - - * 2014-09-23 David Henderson - Removed prefix, added example of details object. - - * 2014-09-23 David Henderson - No good reason to skip a class - - * 2014-09-21 Matías Aguirre - Don't update a setting value. Refs #378. Refs #377 - - * 2014-09-21 Tim Savage - Update installing.rst - - * 2014-09-21 Tim Savage - Update README.rst - - * 2014-09-18 Matías Aguirre - Print arguments in the debug pipeline - - * 2014-09-16 Stefan Kröner - Allow more Trello settings - - * 2014-09-12 Matías Aguirre - Add debug pipeline to example app - - * 2014-09-12 Matías Aguirre - Update snippet - - * 2014-09-12 David Henderson - Updated to use latest api wrapper - - * 2014-09-11 Matías Aguirre - Flag dev version - - * 2014-09-11 Matías Aguirre - v0.2.1 - - * 2014-09-11 Matías Aguirre - Take into account inconsistent instagram responses - - * 2014-09-11 Matías Aguirre - Mension product-name google requirement - - * 2014-09-11 Matías Aguirre - Flag dev version - - * 2014-09-11 Matías Aguirre - v0.2.0 - - * 2014-09-11 Matías Aguirre - Restore @strategy decorator with warning message - - * 2014-09-09 Tsung Hung - updated the docs to add migrations for 1.7 while updated a constant so the - warning message does not appear when running command line - - * 2014-09-09 Tsung Hung - updated the docs to add migrations for 1.7 while updated a constant so the - warning message does not appear when running command line - - * 2014-09-07 Amol Kher - Jawbone needs params instead of data as requests - - * 2014-09-07 Caio Ariede - Support for MineID.org - - * 2014-09-03 Matías Aguirre - Added commets detailing pipeline functionality. Refs #361 - - * 2014-08-31 Matías Aguirre - Enable state parameter for angel.co and spotify.com backends. Fixes #367 - - * 2014-08-29 Matías Aguirre - PEP8 - - * 2014-08-29 Matías Aguirre - PEP8 - - * 2014-08-27 Gianluca Pacchiella - Fix typo - - * 2014-08-27 Clinton Blackburn - Updated OpenId Connect Test Mixin - - * 2014-08-25 Matt Luongo - Use a more flexible South user migration approach. - - * 2014-08-21 Matt Luongo - Remove South from mandatory requirements. - - * 2014-08-21 Matt Luongo - Split up the Django 1.7+ & South migrations. - - * 2014-08-21 Max Nanis - Small grammatical edit - - * 2014-08-19 Martey Dodoo - Fix repository links in thanks document. - - * 2014-08-18 Parker Phinney - changed default behavior of SESSION_EXPIRATION setting - - * 2014-08-18 Ross Crawford-d'Heureuse - added goclio - - * 2014-08-16 Matías Aguirre - Link/img change - - * 2014-08-16 Matías Aguirre - RTD badge - - * 2014-08-16 Matías Aguirre - PEP8 - - * 2014-08-15 Matías Aguirre - Support passwordless schema on mail validation pipeline - - * 2014-08-14 Matías Aguirre - Fix backend reference. Fixes #350 - - * 2014-08-12 = <=> - Add pushbullet backends - - * 2014-08-09 Matías Aguirre - Fix disconnect buttons styles - - * 2014-08-09 Matías Aguirre - PEP8 and fixed tests. Refs #348 - - * 2014-08-08 Clinton Blackburn - Added Open ID Connect base backend - - * 2014-08-08 Josh Probst - numeric index for format - - * 2014-08-07 Matías Aguirre - Fix user syncdb. Refs #342 - - * 2014-08-07 Matías Aguirre - Simplify moves backend code and add documentation. Refs #307 - - * 2014-08-05 Vadym Petrychenko - Update vk.rst - - * 2014-08-02 Matías Aguirre - Landscape conf - - * 2014-08-02 Matías Aguirre - Support redirect_state in OAuth1 backends too (enable twitter by default). - Refs #338 - - * 2014-08-02 Matías Aguirre - Enable DropboxOAuth2 on example app - - * 2014-07-29 Chris Lamb - Also populate Strava name from 'lastname' attribute: - - * 2014-07-29 Chris Lamb - Correct reference to 'firstname' when populating forenames from Strava. - - * 2014-07-29 Chris Lamb - Correct Stava scoping/permissions example. - - * 2014-07-28 Chris Martin - Clean up language in social/tests/README.rst - - * 2014-07-27 Matías Aguirre - Docs about writing custom pipeline functions - - * 2014-07-24 Matt Luongo - Fix an import issue in the Django migrations. - - * 2014-07-24 Matt Luongo - List test requirements. - - * 2014-07-24 Matt Luongo - Support South and Django 1.7+ migrations. - - * 2014-07-23 Mike Anderson - remove debugger - - * 2014-07-23 Mike Anderson - tests for two failing cases, include all kwargs in partial pipeline session - - * 2014-07-22 Mike Anderson - Don't overwrite clean_kwargs with kwargs - - * 2014-07-19 Matías Aguirre - Github for teams backend. Refs #329 - - * 2014-07-16 Nick Sandford - Fixed #327 -- Changed access token method on backend. - - * 2014-07-15 David Grant - Slight retouch to spelling and wordage. - - * 2014-07-15 David Grant - Minor typo. - - * 2014-07-08 Matías Aguirre - Fix FK field descriptor for admin queries. Closes #322 - - * 2014-07-07 Matt Luongo - Use South instead of Django 1.7 migrations. - - * 2014-07-07 Matías Aguirre - Simple makefile for local tasks - - * 2014-07-07 Matías Aguirre - Document django session migration script when moving from - django-social-auth to python-social-auth. Refs #320 - - * 2014-07-07 Matías Aguirre - Make user-agent setting available for all backends. Refs #317 - - * 2014-07-04 Harz-FEAR - fix for AssertionError in pyramid - - * 2014-07-01 Ondrej Slinták - Added Django 1.7 migrations - - * 2014-07-01 davidhubbard - fix PR #317 - - * 2014-07-01 davidhubbard - override request() to fix "429 Too Many Requests" - - * 2014-06-30 Matías Aguirre - Tox runner with pyenv support - - * 2014-06-26 David Henderson - Reinstated get_user_id override - so that we can pull from the details - rather than the response - - * 2014-06-25 Antony Seedhouse - Update django_orm.py - - * 2014-06-25 Antony Seedhouse - Update django_orm.py - - * 2014-06-24 Martey Dodoo - Update link to Django example in documentation. - - * 2014-06-22 Roman Levin - Add note about access_type in docs - - * 2014-06-22 Avi Alkalay - user first_date doesn't belong here - - * 2014-06-21 Avi Alkalay - The Moves app backend - - * 2014-06-18 Matías Aguirre - Initial work towards OpenIdConnect. Refs #300. Refs #284 - - * 2014-06-18 Matías Aguirre - Useful debug pipeling function - - * 2014-06-18 Gabriel Le Breton - text should not go into code block - - * 2014-06-18 Nikolaev Andrey - It was impossible to change the version API Vkotnakte - - * 2014-06-16 Matías Aguirre - Improve django example application look - - * 2014-06-16 Matías Aguirre - Fix key access on instagram backend - - * 2014-06-14 Matías Aguirre - Integrate flask app and flask mongoengine app - - * 2014-06-14 Matías Aguirre - PEP8 - - * 2014-06-14 Matías Aguirre - Fix docstring - - * 2014-06-14 Matías Aguirre - Move common fields to base class in sqlalchemy ORMs. - - * 2014-06-14 Matías Aguirre - Use mongoengin ORM in django me app - - * 2014-06-12 Matías Aguirre - QQ backend - - * 2014-06-09 Josh Hawn - Update docker backend with Docker Hub endpoints - - * 2014-06-08 Matías Aguirre - Add missing module - - * 2014-06-08 Matías Aguirre - Set user backend reference in django app - - * 2014-06-08 Matías Aguirre - Update tests - - * 2014-06-07 Matías Aguirre - Version change (no backward compatible change) - - * 2014-05-26 Matías Aguirre - Refactor backend/strategy to avoid circular dependency - - * 2014-06-07 Matías Aguirre - Support MergeDict and MultiDict in partial cleanup. Refs #291 - -2014-06-07 v0.1.26 -================== - - * 2014-06-07 Matías Aguirre - v0.1.26 - - * 2014-06-07 Matías Aguirre - Fix google-plus scope, support server-side flow - -2014-06-07 v0.1.25 -================== - - * 2014-06-07 Matías Aguirre - v0.1.25 - - * 2014-06-07 Matías Aguirre - Support deprecated and new Google API. Refs #292. Refs #285 - - * 2014-06-07 Matías Aguirre - Fix pipeline example - - * 2014-06-01 Matías Aguirre - Document steam player data saving - - * 2014-05-28 Matías Aguirre - Remove eclipse settings from PR merge - - * 2014-05-26 Matías Aguirre - Document google scopes deprecation. Refs #285 - - * 2014-05-26 Matías Aguirre - Fix title underline - - * 2014-05-26 Matías Aguirre - Make request parameter optional. Refs #286 - - * 2014-05-26 Matías Aguirre - PEP8 - - * 2014-05-24 Devin Sevilla - Rdio API methods use POST - - * 2014-05-22 Michael Godshall - Fixed Django 1.7 admin - - * 2014-05-20 Hector Zhao - avoid updating default settings - - * 2014-05-17 Matías Aguirre - v0.2.0-dev - -2014-05-17 v0.1.24 -================== - - * 2014-05-17 Matías Aguirre - v0.1.24 - - * 2014-05-17 Matías Aguirre - Example for ajax auth. Refs #272, #238 - - * 2014-05-17 Matías Aguirre - Circumvent recursive import issue in admin. Fixes #269 - - * 2014-05-17 Matías Aguirre - Update google scopes, remove the soon to be deprecated ones. Fixes #273 - - * 2014-05-17 Matías Aguirre - Fix title underline in docs - - * 2014-05-17 Ryan Choi - remove mashery stuff from oauth; constrain it to beats - - * 2014-05-17 Ryan Choi - oauth for beats - - * 2014-05-15 Jason Sanford - Add links. - - * 2014-05-15 Ryan Choi - remove commented code for spotify - - * 2014-05-15 Ryan Choi - spotify oauth - - * 2014-05-15 Jason Sanford - Python 2.6-friendly string formatting. - - * 2014-05-15 Jason Sanford - Document MapMyFitness - - * 2014-05-15 Matías Aguirre - Change priority for new user redirect location - - * 2014-05-15 Jason Sanford - Test MapMyFitness backend - - * 2014-05-14 Jason Sanford - Get started with MapMyFitness OAuth2 - - * 2014-05-13 Matías Aguirre - MongoEngine ORM support for flask applications - - * 2014-05-13 swmerko - from http API to https API - - * 2014-05-12 Matías Aguirre - Remove unused import - - * 2014-05-10 Mark Lee - Replace references to python-oauth2 with references to requests-oauthlib - - * 2014-05-08 Smamaxs - get email on login - - * 2014-05-07 Marno Krahmer - Change the authorization url for the xing api - - * 2014-05-06 Matías Aguirre - PEP8 and docs about Facebook Graph 2.0 backends - - * 2014-05-06 Matías Aguirre - Settings to override default scope/attrs and docs about them. Refs #258 - - * 2014-05-06 Daniel Ryan - added new backend classes for Facebook that use the Open Graph 2.0 - endpoints - - * 2014-05-01 Matías Aguirre - PEP8 and logic simplification - - * 2014-05-01 Kyle Richelhoff - Added LoginRadius backend. - - * 2014-04-30 momamene - Add Kakao backend - - * 2014-04-30 Matías Aguirre - Update amazon docs, drop outdate details about bug. Fixes #260 - - * 2014-04-23 Matías Aguirre - Disable redirect_state in strava backend. Fixes #259 - - * 2014-04-23 Matías Aguirre - Allow overrideable values for AX schema attrs and SReg attributes in - OpenId. Fixes #258 - - * 2014-04-23 Matías Aguirre - Refactor fullname, first name and last name generation. Fixes #240 - - * 2014-04-23 Serg Baburin - Using https as required by the API. - - * 2014-04-19 Your Name - User model fields accessors clashes issue solved - - * 2014-04-18 Matías Aguirre - Switch VK OpenAPI to session intead of cookies. - - * 2014-04-14 David Blado - linkedin now requires redirect uris to be verified: - https://developer.linkedin.com/blog/register-your-oauth-2-redirect-urls - - * 2014-04-14 Matías Aguirre - PEP8 - - * 2014-04-14 Hannes Ljungberg - Add Twitch backend - - * 2014-04-10 Matías Aguirre - Remove unused parameters from pipeline prototypes - - * 2014-04-08 Alexander Chernigov - Handle properly refusing when entering via twitter - - * 2014-04-04 Matías Aguirre - Remove doc about deprecated setting. Refs #241 - - * 2014-04-03 Matías Aguirre - Option for open id providers to specify the username key in the values - - * 2014-04-03 Matías Aguirre - Include strava backend in the index - - * 2014-04-02 (cdep) illabout - Fix small spelling mistake. - - * 2014-04-02 Joe Hura - Add support for Vimeo OAuth 2 as part of Vimeo API v3 - - * 2014-04-01 Krishan Gupta - Update settings.rst - - * 2014-04-01 Damien - Incorrect syntax given in the documention - - * 2014-04-01 Matías Aguirre - Fix use-case snippet - - * 2014-04-01 Matías Aguirre - Switch custom redirect state to off in mendeley OAuth2. Closes #234 - - * 2014-04-01 Matías Aguirre - Enable Last.fm in example applications - - * 2014-04-01 Matías Aguirre - Last.fm docs - - * 2014-04-01 Matías Aguirre - Refactor Last.fm backend (simplify code) - - * 2014-03-26 root - Added backend for Last.Fm. There is probably an easier way to implement - this. - - * 2014-03-28 Matías Aguirre - Make exception raise optional with setting. Add tests and docs - - * 2014-03-27 Matías Aguirre - Stop tox on first error - - * 2014-03-27 Matías Aguirre - Avoid passing multiple arguments to disconnect partial pipeline - - * 2014-03-27 Matías Aguirre - Improve partial session cleaner code. Refs #231 - - * 2014-03-27 Piotr Czesław Kalmus - login with bitbucket account, error when any verified email is set - - * 2014-03-27 Matías Aguirre - Link docker docs in backends index - - * 2014-03-27 Matías Aguirre - PEP8 - - * 2014-03-25 Fernando - initial version of docker backend - - * 2014-03-26 Matías Aguirre - Flag dev version - -2014-03-26 v0.1.23 -================== - - * 2014-03-26 Matías Aguirre - v0.1.23 - - * 2014-03-24 Yohan Boniface - OpenStreetMap: no img element if user has no avatar - - * 2014-03-23 Matías Aguirre - Define a custom user model - - * 2014-03-23 Matías Aguirre - Comment about enhanced security flag in Live backend. Refs #218 - - * 2014-03-23 Matías Aguirre - Try to use django messages app, fallback to URL. Fixes #210 - - * 2014-03-23 Matías Aguirre - Don't assign strategy in middleware. Closes #221 - - * 2014-03-23 Matías Aguirre - Pass the social_user to login functions. Refs #190 - - * 2014-03-18 Matías Aguirre - Multiple scopes use case - - * 2014-03-18 Matías Aguirre - Register by token use case - - * 2014-03-16 Matías Aguirre - Fix strava tests and username generation. Refs #217 - - * 2014-03-15 Auston Bunsen - final changes - - * 2014-03-15 Auston Bunsen - updated some docs - - * 2014-03-15 Auston Bunsen - added strava support! - - * 2014-03-15 Matías Aguirre - Remove symlinks. Fixes #177 - - * 2014-03-15 Matías Aguirre - Use stateless mode with Steam. Fixes #200 - - * 2014-03-15 Matías Aguirre - Get social_user instance before login. Refs #190 - - * 2014-03-15 Matías Aguirre - Simplify redirect cleaner method. Closes #191 - - * 2014-03-15 Matías Aguirre - Use forms to disconnect - - * 2014-03-15 Matías Aguirre - Mention localhost limitation on facebook. Closes #207 - - * 2014-03-13 Andrey Kuzmin - Removes flask dependency from webpy_app - - * 2014-03-12 Dave Murphy - Added backend for Ubuntu (One). - - * 2014-03-09 Matías Aguirre - Make oauth_token retrieval optional. Refs #212 - - * 2014-03-08 Baptiste Mispelon - Fixed Django < 1.4 support in context processors. - - * 2014-03-06 Matías Aguirre - Remove bitdeli badge - - * 2014-03-06 Peter Schmidt - Add some missing dependencies for running - `social.apps.django_app.default.tests` - - * 2014-03-01 Matías Aguirre - Link backend docs in index - - * 2014-03-01 Matías Aguirre - Docs about flask error handling - - * 2014-03-01 Matías Aguirre - Mark dev version - - * 2014-03-01 Matías Aguirre - v0.1.22 - - * 2014-02-28 Andrey Kuzmin - Fixes broken email confirmation for SQLAlchemy storage and webpy_app - - * 2014-02-27 Sebastian Bassi - Update mendeley.py - - * 2014-02-27 Matías Aguirre - Don't update user if it's set to None (non-authenticated pipeline - continuation). Refs #198 - - * 2014-02-27 Matías Aguirre - Set is_new flag on pipeline if user is not new. Refs #201 - - * 2014-02-27 Matías Aguirre - Better error message - - * 2014-02-25 Matías Aguirre - Add 'user' to default scope on coinbase backend. Closes #199 - - * 2014-02-24 Matías Aguirre - User USERNAME_FIELD on mongoengine. Closes #197 - - * 2014-02-21 Matías Aguirre - Dev marker - - * 2014-02-18 David Kingman - Removed commit marker - - * 2014-02-13 Matías Aguirre - PEP8, Python3 and example fixes - - * 2014-02-13 Thomas Lovett - fix copy-paste typo in callback url - - * 2014-02-13 Thomas Lovett - add clef to main README - - * 2014-02-14 Yan Kalchevskiy - Fixed a typo. - - * 2014-02-13 Matías Aguirre - Vimeo backend - - * 2014-02-13 Matías Aguirre - Move badge to the top - - * 2014-02-13 Bitdeli Chef - Add a Bitdeli badge to README - - * 2014-02-13 Matías Aguirre - Docs about associate user by email - - * 2014-02-12 Matías Aguirre - Style recent docs - - * 2014-02-13 Joe B. Lewis - added information for FIELDS_STORED_IN_SESSION - - * 2014-02-12 Hassek - removed extra_data override - - * 2014-02-11 Hassek - updated live connection for better support - - * 2014-02-10 Matías Aguirre - CherryPy mention in project index - - * 2014-02-10 Matías Aguirre - CherryPy docs - - * 2014-02-10 Matías Aguirre - Disconnection on example app - - * 2014-02-10 Matías Aguirre - Pass user on disconnect - - * 2014-02-10 Matías Aguirre - Get extra_data from details on openid too - - * 2014-02-10 Matías Aguirre - Mendeley OAuth2 in example app - - * 2014-02-10 Matías Aguirre - Fix Mendeley OAuth2 implementation, use https URLs - - * 2014-02-10 Matías Aguirre - Switch parent class to avoid overrides - - * 2014-02-10 Matías Aguirre - Finishe CherryPy app support and add example application - - * 2014-02-10 Matías Aguirre - Mendeley OAuth2 docs and thanks to Sebastian Bassi (initial author) - - * 2014-02-10 Matías Aguirre - Mendeley OAuth2 backend - - * 2014-02-10 Matías Aguirre - Fix AuthFailed calls - - * 2014-02-10 Matías Aguirre - Raise social-auth exception on connection error. Closes #155 - - * 2014-02-10 Matías Aguirre - Parse token if it's an string (keep a compatible API). Refs #180 - - * 2014-02-09 Matías Aguirre - Stick with sure 1.2.3 (higher is broken, I should drop sure) - - * 2014-02-09 Matías Aguirre - Update sure to 1.2.5 - - * 2014-02-09 Matías Aguirre - Fix LinkedIn OAuth2 backend, pass access token parameter in querystring. - Closes #181 - -2014-02-05 v0.1.21 -================== - - * 2014-02-05 Matías Aguirre - v0.1.21 - - * 2014-02-05 Matías Aguirre - Fix iexact field lookup. Refs #179 - - * 2014-02-05 Matías Aguirre - Restore BackendWrapper to avoid session issues (this backend is - deprecated). Refs #128 - - * 2014-02-05 Matías Aguirre - Case insensitive query on django. Closes #179 - - * 2014-02-03 Matías Aguirre - Exclude sure broken version 1.2.4 - - * 2014-02-01 Michisu, Toshikazu - Add version parameter to foursquare backend - - * 2014-01-23 Matías Aguirre - Use response encoding only when available. Refs #173 - - * 2014-01-21 Matías Aguirre - Add pixelpin to backends index - - * 2014-01-21 Matías Aguirre - Ensure encode() before md5 call for python3. Closes #168 - - * 2014-01-21 lukos - Added PixelPin to list of providers - - * 2014-01-21 Matías Aguirre - PEP8, file formats and line lengths fixes - - * 2014-01-21 luke - Add documentation for PixelPin - - * 2014-01-21 luke - Added new PixelPin provider. - - * 2014-01-20 Matías Aguirre - Use same DB name as other examples - - * 2014-01-20 Yasin Aktimur - Serializer changed. - - * 2014-01-18 Matías Aguirre - Support Weibo domain as username by setting. Closes #164 - - * 2014-01-18 Matías Aguirre - Snippet to get people from circles on Google+ - - * 2014-01-17 Matías Aguirre - Override get_user_id on tumblr backend. Refs #136 - -2014-01-17 v0.1.20 -================== - - * 2014-01-17 Matías Aguirre - v0.1.20 - - * 2014-01-17 Matías Aguirre - Decode bytes on Python3 otherwise it breaks session saving on Django. Refs - #139 - - * 2014-01-16 Matías Aguirre - Fix linkedin docs about attributes names. Closes #161 - - * 2014-01-16 Matías Aguirre - Also support old keys format in linkedin backend for basic data - -2014-01-16 v0.1.19 -================== - - * 2014-01-16 Matías Aguirre - v0.1.19 - - * 2014-01-16 Matías Aguirre - Generate packages names dynamically - -2014-01-16 v0.1.18 -================== - - * 2014-01-16 Matías Aguirre - v0.1.18 - - * 2014-01-15 Matías Aguirre - Raise missing parameter error in facebook. Refs #153 - - * 2014-01-15 harshiljain - AUTHORIZATION_URL changed to https - - * 2014-01-14 Matías Aguirre - PEP8 - - * 2014-01-14 Javier G. Sogo - stores 'access_token' for GooglePlusAuth - - * 2014-01-14 Javier G. Sogo - for FacebookOAuth2::process_revoke_token_response call super (solves type - with 'status_code') and custom processing - - * 2014-01-14 Javier G. Sogo - moved revoking stuff to OAuthAuth class (should it be moved to BaseAuth?) - - * 2014-01-13 Max Tepkeev - odnoklassniki backend iframe app fix - - * 2014-01-10 xen - Cleanup docs - - * 2014-01-10 xen - Simplify SQLAlchemy API usage - - * 2014-01-10 xen - Update to follow current state in documentations - - * 2014-01-08 Roberto Robles - Remove SOCIAL_AUTH prefix on redirect_uri function - - * 2014-01-08 Roberto Robles - Fixed issue with redirect_uri with https - - * 2014-01-08 Matías Aguirre - Link Taobao docs on backends index - - * 2014-01-08 Matías Aguirre - Docs styling and PEP8 - - * 2014-01-08 Jichao Ouyang - taobao docs - - * 2014-01-08 Jichao Ouyang - get token with POST method - - * 2014-01-07 Matías Aguirre - PEP8 and cleanups. Refs #145 - - * 2014-01-07 Matías Aguirre - Move URLs gathering to helper - - * 2014-01-07 Matías Aguirre - Fix dox underline - - * 2014-01-07 Jichao Ouyang - remove unused import - - * 2014-01-07 Jichao Ouyang - add to django example - - * 2014-01-07 Jichao Ouyang - add support for taobao - - * 2014-01-06 Adam Coddington - Updating readme to proclaim OAuth2 support for Dropbox. - - * 2014-01-06 Adam Coddington - Updating Dropbox documentation to include notes regarding OAuth2 support. - - * 2014-01-06 Adam Coddington - Adding Dropbox OAuth2 Support. - - * 2014-01-06 Edwin Knuth - increasing length of salt field for django apps, fixes #141 - - * 2014-01-06 Matías Aguirre - Simplify partial handling on actions - - * 2014-01-06 Matías Aguirre - Updated readme with other dependencies. Closes #140 - - * 2014-01-06 Jichao Ouyang - add support for taobao - - * 2014-01-04 Matías Aguirre - Always send email validations is required - - * 2014-01-04 Matías Aguirre - Move extra-data logic to base clase - - * 2014-01-02 Matías Aguirre - Fix ID_KEY for Tumblr backend. Refs #136 - - * 2014-01-02 Matías Aguirre - Use cases doc. Refs #137 - - * 2014-01-01 Matías Aguirre - Fix docstring. Refs #136 - - * 2013-12-28 Matías Aguirre - Line chars limit in docs. Refs #135 - - * 2013-12-28 Matías Aguirre - PEP8. Refs #135 - - * 2013-12-28 Xmypblu - Add support for OpenStreetMap OAuth - - * 2013-12-27 Matías Aguirre - Update porting docs regarding session value - - * 2013-12-27 Matías Aguirre - PEP8 - - * 2013-12-26 Nicolas Cortot - Support for MongoEngine authentication using Custom User Model - - * 2013-12-25 Nick Sullivan - Update reddit.py - - * 2013-12-17 Jay Parlar - Tiny typo fix - - * 2013-12-16 maxtepkeev - fix session expiration in vk backend - - * 2013-12-11 Kevin Tran - Added support for named URLs and URL translation using the django built-in - resolve_url before giving the url to tje HttpResponseRedirect. See also - https://code.djangoproject.com/ticket/15552 - - * 2013-12-11 Bob Alcorn - Updated pipeline example to include externalized auth; - - * 2013-12-09 Matías Aguirre - Avoid broken email entries on yahoo API. Closes #125 - - * 2013-12-09 Matías Aguirre - Allow unauthorized token retrieval/storage overrideable. Refs #111 - - * 2013-12-07 Matías Aguirre - Constant type compare on HMAC signatures. Closes #122 - - * 2013-12-07 monkut - Removed non-ascii character from author string - - * 2013-12-06 Hans - Add test backends to the package. - - * 2013-12-06 Rodrigue Villetard - Missing trailing slash on complete url - - * 2013-12-03 Matías Aguirre - PEP8. Refs #116 - - * 2013-12-03 Stephen McDonald - Add refs to getpocket.com in readme + docs - - * 2013-12-03 Stephen McDonald - getpocket.com backend - - * 2013-12-02 Matías Aguirre - Helper to get current backend instance. Refs #114 - - * 2013-11-30 Matías Aguirre - Set current strategy on pyramid app - - * 2013-11-30 Matías Aguirre - Simplify pyramid settings access - - * 2013-11-30 Matías Aguirre - Set current strategy on webpy and flask apps - - * 2013-11-29 Matías Aguirre - PEP8 - - * 2013-11-28 Matías Aguirre - Link to backends docs in the modules instead of repeating the docs. Refs - #107 - - * 2013-11-28 Matías Aguirre - Yammer docs - - * 2013-11-28 Matías Aguirre - Improves to Yahoo docs - - * 2013-11-28 Matías Aguirre - Xing docs - - * 2013-11-28 Matías Aguirre - Trello docs - - * 2013-11-28 Matías Aguirre - Podio docs - - * 2013-11-28 Matías Aguirre - Mendeley docs - - * 2013-11-28 Matías Aguirre - Fix backends index order - - * 2013-11-28 Matías Aguirre - LiveJournal docs - - * 2013-11-28 Matías Aguirre - Jawbone docs - - * 2013-11-28 Matías Aguirre - Foursquare backend docs - - * 2013-11-28 Matías Aguirre - Fitbit docs - - * 2013-11-28 Matías Aguirre - Fedora openid docs - - * 2013-11-28 Matías Aguirre - Fix douban oauth1 title - - * 2013-11-28 Matías Aguirre - Dailymotion docs - - * 2013-11-28 Matías Aguirre - File format fix to coinbase docs - - * 2013-11-28 Matías Aguirre - Fix backends order - - * 2013-11-28 Matías Aguirre - BelgiumEID docs - - * 2013-11-28 Matías Aguirre - AOL docs - - * 2013-11-26 Norton Wang - fix uid in coinbase oauth - - * 2013-11-23 Norton Wang - add coinbase docs, add runkeeper docs to index - - * 2013-11-23 Norton Wang - add coinbase oauth - - * 2013-11-23 Norton Wang - Add more examples to django_example, alphabetize, fix some grammar - - * 2013-11-21 Matías Aguirre - Fix setting name in docs. Refs #97 - - * 2013-11-21 Matías Aguirre - Move default pipeline definitions to constants for easy import. Refs #99 - - * 2013-11-21 josseph - Update weibo.py - - * 2013-11-20 Jesse Pollak - adds clef as a login provider - - * 2013-11-20 maxtepkeev - Make vk-app backend to retrieve additional user data in respect to the - *_EXTRA_DATA setting - - * 2013-11-19 Matías Aguirre - Fix typo - - * 2013-11-19 Matías Aguirre - Mention callback URL definition on linkedin when using oauth2. Refs #58 - - * 2013-11-18 Matías Aguirre - Include backend name in setting if backend is defined. Refs #95 - - * 2013-11-18 Matías Aguirre - PEP8 and simplifications. Refs #92 - - * 2013-11-18 Matías Aguirre - Restore prvious link, fix schema in readthedocs link. Refs #93 - - * 2013-11-17 Sahil Gupta - Updated README to point to the latest docs on Read The Docs. - - * 2013-11-16 Marios - Google Plus backend allows for a server-side flow that can grant a refresh - token that can be subsequently used to perform operations on behalf of the - user, even if the user is not online. - - * 2013-11-14 Matías Aguirre - Replace format call with string join. Closes #91 - - * 2013-11-14 Juan Riaza - a better way - - * 2013-11-14 Juan Riaza - fitbit uid - - * 2013-11-13 Matías Aguirre - Fix OpenId PAPE max age check. Closes #89 - - * 2013-11-13 Matías Aguirre - Changelog update - -2013-11-13 v0.1.17 -================== - - * 2013-11-13 Matías Aguirre - v0.1.17 - - * 2013-11-13 Matías Aguirre - Support remember flag when calling login on flask app - - * 2013-11-13 Nitish Rathi - Use strategy.backend.name instead of strategy.backend_name - - * 2013-11-13 Nitish Rathi - Use strategy.backend.name instead of strategy.backend_name - - * 2013-11-13 Nitish Rathi - Use strategy.backend.name instead of strategy.backend_name - - * 2013-11-13 Matías Aguirre - Update ChangeLog - - * 2013-11-13 Matías Aguirre - Define exception to signal a backend-not-found error. Refs #83 - - * 2013-11-11 Алексей - Raise Http404 in django auth view when the backend is not found - - * 2013-11-10 Matías Aguirre - Remove BackendWrapper reference and set current-strategy cache to access it - - * 2013-11-10 Matías Aguirre - Set social_ prefix on request attribute to avoid conflicts with other apps. - Keep social attribute if not set (backward compatibility) - - * 2013-11-09 Matías Aguirre - Update github docs regarding callback URL. Closes #66 - - * 2013-11-08 yegle - Mod: URL for registering Windows Live key/secret - - * 2013-11-08 Matías Aguirre - Fix association id. Closes #78 - - * 2013-11-07 Matías Aguirre - Mention method used - - * 2013-11-07 Matías Aguirre - Typo fix - - * 2013-11-07 Matías Aguirre - Update middleware docs - -2013-11-07 v0.1.16 -================== - - * 2013-11-07 Matías Aguirre - v0.1.16 - - * 2013-11-07 Matías Aguirre - Remove unused vars - - * 2013-11-07 Matías Aguirre - Remove or check which always default to settings.DEBUG if RAISE_EXCEPTIONS - was False - - * 2013-11-07 Michal Čihař - Include actions module in distribution - - * 2013-11-06 Matías Aguirre - Ensure IDs to openid association removal. Closes #76 - - * 2013-11-05 Branden Rolston - Update partial from session with newer kwargs. - - * 2013-11-05 Branden Rolston - Use mock. - - * 2013-11-05 Matías Aguirre - Link to tornado docs - - * 2013-11-05 Matías Aguirre - Fix to douban access token retrieval method. Closes #72 - - * 2013-11-05 Matías Aguirre - Custom user model in mongoengine example app. Refs #70 - - * 2013-11-05 Matías Aguirre - PEP8 - - * 2013-11-05 Matías Aguirre - Mention tornado on readme and docs intro - - * 2013-11-05 Axel Haustant - Talk about tox in test documentation - - * 2013-11-05 Axel Haustant - Upgrade HTTPretty for OSX/Requets 2.0 compatibility - - * 2013-11-05 Axel Haustant - Added tox configuration - - * 2013-11-05 Axel Haustant - quote message for url inclusion - - * 2013-11-04 Branden Rolston - Return the updated dict. - - * 2013-11-04 Matías Aguirre - v0.1.15 - -2013-11-04 v0.1.15 -================== - - * 2013-11-04 Matías Aguirre - v0.1.15 - - * 2013-11-04 Matías Aguirre - Test runkeeper backend - - * 2013-11-02 Matías Aguirre - PEP8 and implement missing method - - * 2013-11-02 Martin Santos - Removed prints - - * 2013-11-02 Matías Aguirre - Fixes to Tornado application (mostly cookies handling) - - * 2013-11-02 Martin Santos - WIP: More changes - - * 2013-11-02 Martin Santos - WIP: strategy and example app - - * 2013-11-01 Martin Santos - Initial tornado support - - * 2013-10-28 Andrey Mitroshin - Function user_data returns list. This leads to exception in - social/backends/oauth.py (line 340): "ValueError, dictionary update - sequence element #0 has length 31; 2 is required". Taking 1st elementt of - that list fixes the error. - - * 2013-10-23 Jason Sanford - Add RunKeeper. - - * 2013-10-23 Matías Aguirre - Fix reference - - * 2013-10-23 Matías Aguirre - Add missing links - - * 2013-10-23 Matías Aguirre - Support python3-openid last changes on Association class - - * 2013-10-23 Hannes Ljungberg - Make partial_pipeline JSON serializable for django 1.6 - - * 2013-10-15 Matías Aguirre - Add missing attribute to flask storage - - * 2013-10-15 Matías Aguirre - Fix typo. Closes #61 - - * 2013-10-14 Matías Aguirre - Small fixes to apfuel doc. Refs #59 - - * 2013-10-14 z4r <24erre@gmail.com> - Appsfuel doc from dsa to psa - - * 2013-10-14 z4r <24erre@gmail.com> - Appsfuel doc from dsa to psa - - * 2013-10-10 Matías Aguirre - Make BackendWrapper respect backends interface. Refs #53 - - * 2013-10-10 Matías Aguirre - Docs regarding Django 1.6 and backends enforced into - AUTHENTICATION_BACKENDS. Refs #53 - - * 2013-10-10 Matías Aguirre - Try setting with backend name and without - - * 2013-10-10 Matías Aguirre - Remove backend_name property - - * 2013-10-10 Matías Aguirre - Fix arguments on refresh_token() method. Refs #52 - - * 2013-10-10 Matías Aguirre - Make backend_name a property. Refs #52 - - * 2013-10-10 Matías Aguirre - Pass the correct name to strategy setting method - - * 2013-10-08 Michal Čihař - Add openSUSE OpenID login - - * 2013-10-08 Matías Aguirre - Fix url check type. Refs #49 - - * 2013-10-08 Matías Aguirre - PEP8 and small simplification on sanitize_url check. Refs #49 - - * 2013-10-08 Matías Aguirre - Clean every mergedict data type - - * 2013-10-08 Matías Aguirre - Force dict type over response (convert mergedict types) - - * 2013-10-07 Matías Aguirre - Enforce dict() on values - - * 2013-10-07 Daniel Barreto - Check for None when `sanitize_redirect` returns in `do_complete`. - - * 2013-10-07 Daniel Barreto - Make `sanitize_redirect` aware of possible proxies. - -2013-10-07 v0.1.14 -================== - - * 2013-10-07 Matías Aguirre - v0.1.14 - - * 2013-10-07 Matías Aguirre - Fix encoding string between python2 and 3 - - * 2013-10-07 Matías Aguirre - Always wrap openid session value - - * 2013-10-07 Matías Aguirre - Encode value to avoid Python3 errors. Refs #776 - - * 2013-10-06 Matías Aguirre - Google plus sign in docs - - * 2013-10-06 Matías Aguirre - Fix links on google docs - - * 2013-10-06 Matías Aguirre - Google+ Sign In backend example - - * 2013-10-06 Matías Aguirre - Working Google+ Sign In backend - - * 2013-10-06 Matías Aguirre - Move process_error() to upper class - - * 2013-10-05 Matías Aguirre - Enable json serializer on example app - - * 2013-10-04 Markus Holtermann - Fixes #45 -- AttributeError while resolving the user model in Django - - * 2013-10-03 Matías Aguirre - Serialize only well-known types, rename function to remark the usage. Refs - #36 - - * 2013-10-03 Matías Aguirre - Use seconds to set session expiration. Refs #36 - - * 2013-10-02 Matías Aguirre - Rename verification code parameter to avoid clashing with backends - parameters - - * 2013-10-02 nvbn - Fix work with django 1.6 - - * 2013-10-02 nvbn - Make JSONField compatible with python 3 - - * 2013-10-02 Matías Aguirre - Update docs regarding yahoo keys. Refs #43 - - * 2013-09-29 Matías Aguirre - Small code changes - - * 2013-09-29 Matías Aguirre - Remove path from urls - - * 2013-09-28 Matías Aguirre - Only run tests on social/tests - - * 2013-09-28 Matías Aguirre - Mention pyramid in keywords - - * 2013-09-28 Matías Aguirre - Remove python-coveralls - - * 2013-09-28 Matías Aguirre - Django tests - - * 2013-09-28 Matías Aguirre - Move base classes to directories - - * 2013-09-27 Matías Aguirre - Remove dbref=True from ReferenceField. Refs #42 - - * 2013-09-26 Matías Aguirre - Process facebook errors on complete. Refs #40 - - * 2013-09-24 Matías Aguirre - White list setting - - * 2013-09-23 Matías Aguirre - Simplified django example applications - - * 2013-09-23 Matías Aguirre - Add mongoengine to requirements - - * 2013-09-22 Matías Aguirre - Mongoengine example - - * 2013-09-22 Matías Aguirre - Fix url for django mongoengine support, add str_id() helper - - * 2013-09-22 Matías Aguirre - Refactor common code on username/email backends tests - - * 2013-09-22 Matías Aguirre - Email backend test - - * 2013-09-22 Matías Aguirre - Remove print line - - * 2013-09-22 Matías Aguirre - Code model on tests - - * 2013-09-22 Matías Aguirre - build_absolute_uri test case - - * 2013-09-22 Matías Aguirre - Small simplification on disconnect action - - * 2013-09-22 Matías Aguirre - Username backend test case - - * 2013-09-22 Matías Aguirre - Test suite defined on setup.py - - * 2013-09-22 Matías Aguirre - Move tests inside the social package - - * 2013-09-22 Matías Aguirre - Remove coveralls - - * 2013-09-22 Matías Aguirre - First try with coveralls - - * 2013-09-22 Matías Aguirre - More badges to README.rst - - * 2013-09-22 Matías Aguirre - Remove python 2.5 support from setup.py - - * 2013-09-22 Matías Aguirre - Doc clarification - - * 2013-09-22 Matías Aguirre - Changelog update - -2013-09-22 v0.1.13 -================== - - * 2013-09-22 Matías Aguirre - v0.1.13 - - * 2013-09-22 Matías Aguirre - Move common code to base class - - * 2013-09-22 Matías Aguirre - Small improve to email partial pipeline on example app - - * 2013-09-22 Matías Aguirre - Fix titles and sections on some docs - - * 2013-09-22 Matías Aguirre - Link to pipeline section - - * 2013-09-22 Matías Aguirre - Code model docs - - * 2013-09-22 Matías Aguirre - Move email validation docs to pipeline.rst - - * 2013-09-21 Matías Aguirre - Improve email validation to only validate new accounts - - * 2013-09-21 Matías Aguirre - Email and Username backends docs - - * 2013-09-21 Matías Aguirre - Docstring fix - - * 2013-09-21 Matías Aguirre - Username backend - - * 2013-09-21 Matías Aguirre - Drop password - - * 2013-09-21 Matías Aguirre - Email validation only on new accounts - - * 2013-09-21 Matías Aguirre - Drop password support, let that to developers in pipeline - - * 2013-09-21 Matías Aguirre - Validate email only if needed - - * 2013-09-21 Matías Aguirre - Small improvement on partial decorator - - * 2013-09-21 Matías Aguirre - Enable password on user save fields - - * 2013-09-21 Matías Aguirre - Email validation on django example app - - * 2013-09-21 Matías Aguirre - Email validation pipeline and strategy functions - - * 2013-09-21 Matías Aguirre - Verification code models - - * 2013-09-21 Matías Aguirre - Django example for email auth - - * 2013-09-21 Matías Aguirre - Initial email auth backend (no email validation yet) - - * 2013-09-21 Matías Aguirre - Remove complex class definition on django json field. Refs #35 - - * 2013-09-20 Matías Aguirre - Define 'POST' method for access token retrieval in Odnoklassniki backend. - Refs #33 - - * 2013-09-19 Matías Aguirre - Sanitize redirects on complete before sending it - - * 2013-09-18 Matías Aguirre - Link to Box doc - - * 2013-09-18 Matías Aguirre - Link Pyramid and add it to list on README - - * 2013-09-18 Matías Aguirre - Changelog update - - * 2013-09-18 Matías Aguirre - Changelog file - - * 2013-09-17 Jonathan Tsai - Update README.rst - - * 2013-09-17 Matías Aguirre - Update pipeline docs with disconnection-pipeline feature - - * 2013-09-17 Jonathan Tsai - Update pipeline.rst - - * 2013-09-15 Matías Aguirre - Ensure request in the pipeline - - * 2013-09-15 Jonathan Tsai - Update README.rst - - * 2013-09-15 Matías Aguirre - Add requirements for pyramid example app - - * 2013-09-15 Matías Aguirre - Pyramid docs - - * 2013-09-15 Matías Aguirre - Pyramid example app - - * 2013-09-15 Matías Aguirre - Pyramid strategy and application - - * 2013-09-15 Matías Aguirre - Move build_absolute_uri base code to utils - - * 2013-09-15 Matías Aguirre - Remove unused import - - * 2013-09-15 Matías Aguirre - Change lists to tuples - - * 2013-09-15 Matías Aguirre - Commit session only if flagged - - * 2013-09-14 Matías Aguirre - Return the poped value - - * 2013-09-14 Matías Aguirre - Return the poped value - - * 2013-09-14 Matías Aguirre - Remove prints - - * 2013-09-14 Matías Aguirre - Partial pipeline on django example - -2013-09-13 v0.1.12 -================== - - * 2013-09-13 Matías Aguirre - v0.1.12 - - * 2013-09-12 Matías Aguirre - Fix get_social_auth_for_user on mongoengine storage - - * 2013-09-12 Matías Aguirre - Fix rst syntax. Fix site index linking - - * 2013-09-12 Matías Aguirre - More settings fixes. Refs #29 - - * 2013-09-12 Matías Aguirre - Review setting names on docs. Refs #29. Refs #28. - - * 2013-09-12 Matías Aguirre - Review setting names on docs. Refs #29 - - * 2013-09-10 Matías Aguirre - PEP8 and code simplification. Refs #26 - - * 2013-09-09 Roman - Added workaround for REDIRECT_STATE and urlencoding. - - * 2013-09-09 Roman - Fixed auth redirect URL for BaseOauth2 always redirecting wrong. OAuth2 - providers expect the url to be an unquoted value. - - * 2013-09-09 Matías Aguirre - Fix thisismyjam test - - * 2013-09-09 Matías Aguirre - PEP8 on thisismyjam backend - - * 2013-09-08 Rob McQueen - changing back to default KEY/SECRET naming - - * 2013-09-08 Sam Kuehn - Remove data that should should not be stored in extra_data - - * 2013-09-08 Rob McQueen - change title of thisismyjam docs - - * 2013-09-08 Rob McQueen - Adding Support For ThisIsMyJam - - * 2013-09-08 Matías Aguirre - Disconnect pipeline, move details/uid extraction to pipeline methods too. - Refactor pipeline run code - - * 2013-09-08 Matías Aguirre - New user redirect test - - * 2013-09-08 Sam Kuehn - Add box.net to readme - - * 2013-09-08 Sam Kuehn - Add box.net to list off supported providers - - * 2013-09-08 Sam Kuehn - Add box.net support - - * 2013-09-08 Matías Aguirre - Test broken disconnect - - * 2013-09-08 Sam Kuehn - Ignore .DS_Store files - - * 2013-09-08 Matías Aguirre - Custom user model for mongoengine backends - - * 2013-09-07 Matías Aguirre - Thanks doc - - * 2013-09-07 Matías Aguirre - Keep old data on refresh token if no new data was received - - * 2013-09-07 Matías Aguirre - Fix extra data case for tuple with single value - - * 2013-09-07 Matías Aguirre - Set request if not present on pipeline continuation, fix args passed to - continue_pipeline - -2013-09-03 v0.1.11 -================== - - * 2013-09-03 Matías Aguirre - v0.1.11 - - * 2013-09-03 Matías Aguirre - Enforce list on pipeline method - - * 2013-09-02 Matías Aguirre - Drop regex search on steam id. Refs #23 - - * 2013-09-02 Matías Aguirre - Steam link - - * 2013-09-01 Matías Aguirre - Generic whitelist tests - - * 2013-09-01 Matías Aguirre - Refactor email/domain whitelist checking, make it generic to all backends - - * 2013-09-01 Matías Aguirre - More revoke token test on dummy backend - - * 2013-09-01 Matías Aguirre - Simplifications to revoke token code - - * 2013-09-01 Matías Aguirre - Revoke token test - - * 2013-09-01 Matías Aguirre - Fixes to revoke token code - - * 2013-09-01 Matías Aguirre - Fix test name - - * 2013-09-01 Matías Aguirre - Rewrite conditions on user pipeline - - * 2013-08-31 Matías Aguirre - Associate by email test - - * 2013-08-31 Matías Aguirre - Rewrite if - - * 2013-08-31 Matías Aguirre - Move common testing code to base class - - * 2013-08-31 Matías Aguirre - Enable broken steam test - - * 2013-08-30 Matías Aguirre - Comment test, needs more investigation - - * 2013-08-30 Matías Aguirre - Test @partial pipeline decorator - - * 2013-08-30 Matías Aguirre - Steam test on missing steam id - - * 2013-08-30 Matías Aguirre - Fix github emails retrieval. Add tests - - * 2013-08-30 Matías Aguirre - Change missing %s to format() call - - * 2013-08-30 Matías Aguirre - Switch %s in favor of .format - - * 2013-08-30 Matías Aguirre - Pass parameters by name - - * 2013-08-29 Matías Aguirre - Fix deletion in sqlalchemy orm - - * 2013-08-29 Matías Aguirre - Enable reddit backend in example - - * 2013-08-29 Matías Aguirre - Add logout route, increase flask version in requirements - - * 2013-08-29 Matías Aguirre - Remove unused parameters - - * 2013-08-29 Matías Aguirre - Fix format string - - * 2013-08-29 Matías Aguirre - Fetch emails if the scope allows it, support future response from API too. - - * 2013-08-29 Matías Aguirre - Port token revoke on disconnection - - * 2013-08-29 Matías Aguirre - Set lazy backref to user model in sqlalchemy orm - - * 2013-08-29 Matías Aguirre - Move disconnect common code to base class - -2013-08-29 v0.1.10 -================== - - * 2013-08-29 Matías Aguirre - v0.1.10 - - * 2013-08-29 Matías Aguirre - PEP8 - - * 2013-08-29 Matías Aguirre - Port associate by email pipeline entry - - * 2013-08-29 Matías Aguirre - Fix shopify and vk definitions - -2013-08-29 v0.1.9 -================= - - * 2013-08-29 Matías Aguirre - v0.1.9 - - * 2013-08-29 Matías Aguirre - Allow to override strategy getter - - * 2013-08-29 Matías Aguirre - Port linkedin force profile language setting from DSA - - * 2013-08-28 Matías Aguirre - Port slug func override from DSA, define identity funcs to avoid extra ifs - - * 2013-08-28 Matías Aguirre - Requests oauthlib still broken - - * 2013-08-28 Matías Aguirre - Requests oauthlib still broken - - * 2013-08-28 Matías Aguirre - Port HTTPS ensure code from DSA - - * 2013-08-28 Matías Aguirre - Port fields length config by settings - - * 2013-08-28 Matías Aguirre - Wording fix - - * 2013-07-15 Matías Aguirre - Fix Steam backend steam id retrieval - - * 2013-07-15 Matías Aguirre - Fix super call. - - * 2013-07-15 Matías Aguirre - Django imports for version lower than 1.4 and higher - - * 2013-07-15 Florian Auroy - Pass synchronize_session='fetch' to delete. - - * 2013-07-15 Florian Auroy - Commit the session after removing an association. - - * 2013-07-14 Matías Aguirre - Django admin conf for default django app - - * 2013-07-14 Matías Aguirre - Port ExactTarget backend from DSA - - * 2013-07-14 Matías Aguirre - Port Jawbone backend from DSA - - * 2013-07-14 Matías Aguirre - Port Fedora backend from DSA - - * 2013-07-14 Matías Aguirre - Port Belgium e-ID backend from DSA - - * 2013-07-14 Matías Aguirre - Port AppsFuel backend from DSA - - * 2013-07-14 Matías Aguirre - Port AOL backend from DSA - - * 2013-07-14 Matías Aguirre - Dummy change - - * 2013-07-13 Matías Aguirre - Reduce the code in openid wrapper for flask 0.10 - -2013-07-13 v0.1.8 -================= - - * 2013-07-13 Matías Aguirre - v0.1.8 - - * 2013-07-13 Matías Aguirre - Add method to determine if current user is allowed to login - - * 2013-06-30 Florian Auroy - Fix OpenId auth with Flask 0.10 - - * 2013-06-20 Matías Aguirre - Add instance to session before commiting it - - * 2013-06-20 Orchestrator81 - Updated the CodersClan button to the right repo_id - - * 2013-06-20 Orchestrator81 - Add CodersClan button to the Readme file - - * 2013-06-13 Martin Santos - Better aproach to the default value of response - - * 2013-06-13 Martin Santos - No mandatory param "response" in do_auth of facebook backend - - * 2013-06-13 Martin Santos - Forgot to declare the param response at the top of the function - - * 2013-06-13 Martin Santos - Pass expected parameter response instead expires - - * 2013-06-12 Matías Aguirre - Pass username as named parameter - -2013-06-03 v0.1.7 -================= - - * 2013-06-03 Matías Aguirre - v0.1.7 - - * 2013-06-03 Matías Aguirre - Fix inheritance on flask and sqlalchemy orm - - * 2013-06-03 Matías Aguirre - Pass session into flask app init - -2013-06-03 v0.1.6 -================= - - * 2013-06-03 Matías Aguirre - v0.1.6 - - * 2013-06-03 Matías Aguirre - Enforce db session passing on flask init - -2013-06-01 v0.1.5 -================= - - * 2013-06-01 Matías Aguirre - v0.1.5 - - * 2013-06-01 Matías Aguirre - Simpler code to convert values to and from session - - * 2013-06-01 Matías Aguirre - Fix is_new flag - - * 2013-06-01 Matías Aguirre - Added @partial decorator, much simpler than adding entries to pipeline - - * 2013-06-01 Matías Aguirre - Clean pipeline after auth process - - * 2013-06-01 Matías Aguirre - Remove is_response() method - - * 2013-06-01 Matías Aguirre - Simpler partial pipeline check - -2013-05-31 v0.1.4 -================= - - * 2013-05-31 Matías Aguirre - v0.1.4 - - * 2013-05-31 Matías Aguirre - Unrestricted user fields on instance creation, defaults to username and - email - -2013-05-31 v0.1.3 -================= - - * 2013-05-31 Matías Aguirre - v0.1.3 - - * 2013-05-30 Matías Aguirre - Avoid version 0.3.2 of requests-oauthlib on python 3 (setup.py) - - * 2013-05-29 Matías Aguirre - Avoid version 0.3.2 of requests-oauthlib on python 3 - - * 2013-05-29 Matías Aguirre - Amazon OAuth2 backend - - * 2013-05-21 dongweiming - Modify trello.py to pass pep8 - - * 2013-05-20 jgsogo - added support for django custom user with no 'username' field - - * 2013-05-20 dongweiming - Add Trello backend support - - * 2013-05-17 Matías Aguirre - PEP8 - - * 2013-05-17 Matías Aguirre - PEP8 - - * 2013-05-17 Matías Aguirre - Pass decoding=None to oauthlib since the default value (utf-8) creates - problems with python-requests on python3 - - * 2013-05-17 George Sakkis - Podio backend: proper way to split the work between user_data() and - get_user_details() - - * 2013-05-17 George Sakkis - Podio backend - - * 2013-05-14 Matías Aguirre - Rename backend import path - - * 2013-05-14 Matías Aguirre - Avoid import error on local_settings - - * 2013-05-14 Matías Aguirre - Add required settings to settings.py - - * 2013-04-23 Matías Aguirre - PEP8 over vk module - - * 2013-04-23 Alexey Boriskin - Adjust examples to the vkontakte -> vk.com rename - - * 2013-04-23 Alexey Boriskin - Adjust documentation to the vkontakte -> vk.com rename - - * 2013-04-23 Alexey Boriskin - Rename vkontakte to vk.com in the code - - * 2013-04-23 Alexey Boriskin - Added test for vkontakte oauth2 backend. - - * 2013-04-22 Alexey Boriskin - Update links and API urls: rename vkontatke.ru to vk.com because of social - network official rename - - * 2013-04-22 Alexey Boriskin - Fixed bug, which prevented VK backend from picking user data (first_name - and last_name) - - * 2013-04-22 Alexey Boriskin - Fix mixed up key and secret - - * 2013-04-22 Matías Aguirre - Freeze httpretty dep since that package breaks python3 support quite often - - * 2013-04-21 Matías Aguirre - Mendeley backend - - * 2013-04-21 Matías Aguirre - Ignore invalid tokens when building setting name - - * 2013-04-21 Matías Aguirre - Yaru OAuth2 backend - - * 2013-04-20 Andrey - changed ACCESS_TOKEN_METHOD to POST - - * 2013-04-04 Matías Aguirre - Support _ setting format too - - * 2013-04-04 Matías Aguirre - Fix persona backend - -2013-04-03 v0.1.2 -================= - - * 2013-04-03 Matías Aguirre - v0.1.2 - - * 2013-04-03 Matías Aguirre - Update tests docs - - * 2013-04-03 Matías Aguirre - Encode string before calling b64 - - * 2013-04-03 Matías Aguirre - Refresh token tests - - * 2013-04-03 Matías Aguirre - Remove unused method - - * 2013-04-03 Matías Aguirre - Reddit backend test - - * 2013-04-03 Matías Aguirre - Dummy space align - - * 2013-04-03 Matías Aguirre - Pipeline tests - - * 2013-04-03 Matías Aguirre - Configurable clean step on usernames. - - * 2013-04-03 Matías Aguirre - Rename pipeline parameter from social_user to social - - * 2013-04-03 Matías Aguirre - Multiple accounts tests, move code from super class to subclass since it's - where they belong - - * 2013-04-03 Matías Aguirre - Rename handles from octocat to foobar on github data/tests - - * 2013-04-03 Matías Aguirre - Rename var to avoid override of function - - * 2013-04-03 Matías Aguirre - Encode seed() to be py3 complaint - - * 2013-04-03 Matías Aguirre - Storage tests - - * 2013-04-03 Matías Aguirre - Exceptions fixes, remove titled_name attribute from backends, use strategy - function instead of storage - - * 2013-04-02 Matías Aguirre - Google whitelist domains/emails tests - - * 2013-04-02 Matías Aguirre - Fix py3 import - - * 2013-04-02 Matías Aguirre - OpenId tests - - * 2013-04-02 Matías Aguirre - Remove unused import - - * 2013-04-02 Matías Aguirre - Remove unused code from google backend - - * 2013-04-02 Matías Aguirre - Simplify steam backend code, enable it on django demo - -2013-04-01 v0.1.1 -================= - - * 2013-04-01 Matías Aguirre - v0.1.1 - - * 2013-04-01 Matías Aguirre - Use a default dict to play with the console and django strategy - - * 2013-03-31 Matías Aguirre - Remove exception handling - - * 2013-03-31 Matías Aguirre - Verify tokens returned by tokes property - - * 2013-03-31 Matías Aguirre - Discard invalid types when cleaning urls - - * 2013-03-31 Matías Aguirre - Utils and expiration_datetime test - - * 2013-03-31 Matías Aguirre - Simplify utc handling on expiration_datetime method - - * 2013-03-31 Matías Aguirre - Fix exception for use with python3 - - * 2013-03-31 Matías Aguirre - Exceptions tests - - * 2013-03-31 Matías Aguirre - Replace __unicode__ with __str__ on exceptions - - * 2013-03-31 Matías Aguirre - Google unique-id support test - - * 2013-03-31 Matías Aguirre - Dummy backend test - - * 2013-03-31 Matías Aguirre - Improve extra_data names handling, remove unused method - - * 2013-03-31 Matías Aguirre - Move common code to base class on linkedin tests - - * 2013-03-31 Matías Aguirre - Fix linkedin test to use json response format - - * 2013-03-31 Matías Aguirre - Rename extra_params to params - - * 2013-03-31 Matías Aguirre - Use linkedin JSON format instead of parsing XML - - * 2013-03-31 Matías Aguirre - Github organization backend improves and tests - - * 2013-03-31 Matías Aguirre - Small improve to yandex first/last name generation - - * 2013-03-31 Matías Aguirre - Test backends info returned by social.backends.utils.user_backends_data - - * 2013-03-30 Matías Aguirre - Tripit 100% coverage tests - - * 2013-03-30 Matías Aguirre - Fix method type on stripe backend - - * 2013-03-30 Matías Aguirre - Stocktwits 100% coverage tests - - * 2013-03-30 Matías Aguirre - Improve soundcloud tests - - * 2013-03-30 Matías Aguirre - Facebook fail on user-data test - - * 2013-03-30 Matías Aguirre - 100% coverage of evernote backend - - * 2013-03-30 Matías Aguirre - Simplify yammer auth_complete code - - * 2013-03-30 Matías Aguirre - Simplify oauth1 and 2 tests code - - * 2013-03-30 Matías Aguirre - Test running script - - * 2013-03-30 Matías Aguirre - Simplify auth error processing - - * 2013-03-29 Matías Aguirre - Add coverage to dependencies - - * 2013-03-29 Matías Aguirre - Backends utils tests - - * 2013-03-29 Matías Aguirre - Reset backends cache if force_load was set to True - - * 2013-03-29 Matías Aguirre - Run tests with coverage - - * 2013-03-29 Matías Aguirre - More tests - - * 2013-03-29 Matías Aguirre - Remove else clause - - * 2013-03-29 Matías Aguirre - Simplify user_data calls (remove try/except blocks) - - * 2013-03-29 Matías Aguirre - Fix partial pipeline arguments to avoid messing with broken pipeline case - - * 2013-03-29 Matías Aguirre - Simplify tokens helper in models/backends since tokens are stored in - desired format already - - * 2013-03-29 Matías Aguirre - Remove stop-pipeline exception (not used at all) - - * 2013-03-29 Matías Aguirre - Initial cherrypy support - - * 2013-03-26 Matías Aguirre - Fix exception raised on skyrock backend - - * 2013-03-26 Matías Aguirre - Reddit backend docs - - * 2013-03-26 Matías Aguirre - Protect request access in case it's None - - * 2013-03-26 Matías Aguirre - Reddit backend - - * 2013-03-26 Matías Aguirre - Fix refresh-token method - - * 2013-03-26 Matías Aguirre - Move strategy loader to outside function to ease strategy creation from - scripts - - * 2013-03-25 Matías Aguirre - Github for organizations backend - - * 2013-03-25 Matías Aguirre - Update twitter doc borrowed from DSA project - - * 2013-03-24 Matías Aguirre - Define scope separators for linkedin oauth1 and oauth2 - - * 2013-03-24 Matías Aguirre - Linkedin OAuth2 docs - - * 2013-03-24 Matías Aguirre - Simplify user_data method on linkedin backends - - * 2013-03-24 Matías Aguirre - Linkedin OAuth2 backend - - * 2013-03-24 Matías Aguirre - Fix setting reference in linkedin docs - - * 2013-03-24 Matías Aguirre - Fix settings references on docs - - * 2013-03-24 Matías Aguirre - Fix linkedin docs - - * 2013-03-24 Matías Aguirre - Fix association get method on tests - - * 2013-03-24 Matías Aguirre - Small rename on openid base code - - * 2013-03-24 Matías Aguirre - Remove unnecessary lines in setUp methods - - * 2013-03-24 Matías Aguirre - Move __init__ code into setUp method - - * 2013-03-24 Matías Aguirre - Partial pipeline tests on actions. Simplify unused code, return backend - stored on session on partial pipelines - - * 2013-03-23 Matías Aguirre - Add travis-ci build status image into README - - * 2013-03-23 Matías Aguirre - Change install method to avoid python2/python3 dependency issues on - travis-ci - - * 2013-03-23 Matías Aguirre - Remove debug print, use print() on docstring - - * 2013-03-23 Matías Aguirre - Add python3.3 to travis conf - - * 2013-03-23 Matías Aguirre - Set sorted fields selectors on linkedin to avoid HTTPretty failure on tests - - * 2013-03-23 Matías Aguirre - Travis YAML - - * 2013-03-23 Matías Aguirre - Update tests readme - - * 2013-03-23 Matías Aguirre - Disconnect test - - * 2013-03-23 Matías Aguirre - Simplify actions tests, add association test - - * 2013-03-23 Matías Aguirre - Add actions tests (login so far) - - * 2013-03-23 Matías Aguirre - Add response class and define is_response() on test strategy - - * 2013-03-23 Matías Aguirre - Save user username in session and verify it on tests - - * 2013-03-23 Matías Aguirre - Define authenticate() method on test strategy - - * 2013-03-23 Matías Aguirre - Simplify authenticate() code on flask and webpy strategies - - * 2013-03-23 Matías Aguirre - Catch custom user attributes in case login() resets the instance - - * 2013-03-23 Matías Aguirre - Move actions module to root module - - * 2013-03-22 Matías Aguirre - Allow already instanced template strategies - - * 2013-03-22 Matías Aguirre - Use setdefault() to set current template strategy on tests - - * 2013-03-22 Matías Aguirre - Use setdefault() to set current template strategy - - * 2013-03-22 Matías Aguirre - Move rendering process into strategy class too - - * 2013-03-18 Matías Aguirre - Tests docs - - * 2013-03-18 Matías Aguirre - Tests readme - - * 2013-03-18 Matías Aguirre - Dropbox backend tests - - * 2013-03-18 Matías Aguirre - Define parameters names on class attributes. Fix Dropbox backend - - * 2013-03-18 Matías Aguirre - Xing backend tests - - * 2013-03-18 Matías Aguirre - Fix xing backend - - * 2013-03-18 Matías Aguirre - Twitter backend test - - * 2013-03-18 Matías Aguirre - Tumblr backend test - - * 2013-03-18 Matías Aguirre - Support POST request method on oauth1 backends tests - - * 2013-03-18 Matías Aguirre - Add option for POST request token method - - * 2013-03-18 Matías Aguirre - Enable tumblr backend in example app - - * 2013-03-18 Matías Aguirre - Skyrock tests - - * 2013-03-18 Matías Aguirre - Fix skyrock backend - - * 2013-03-18 Matías Aguirre - Enable skyrock backend on example app - - * 2013-03-18 Matías Aguirre - Tripit tests - - * 2013-03-18 Matías Aguirre - Readability tests - - * 2013-03-18 Matías Aguirre - Fix readability backend - - * 2013-03-18 Matías Aguirre - Enable readability backend on example app - - * 2013-03-18 Matías Aguirre - Linkedin tests - - * 2013-03-18 Matías Aguirre - Rename test classes to reflect the backend type - - * 2013-03-18 Matías Aguirre - Google OAuth1 test - - * 2013-03-18 Matías Aguirre - Remove empty line - - * 2013-03-18 Matías Aguirre - Flickr tests - - * 2013-03-18 Matías Aguirre - Fix and simplify flickr backend - - * 2013-03-18 Matías Aguirre - Fitbit tests - - * 2013-03-18 Matías Aguirre - Fix fitbit backend - - * 2013-03-18 Matías Aguirre - Evernote tests - - * 2013-03-18 Matías Aguirre - Fix evernote backend - - * 2013-03-18 Matías Aguirre - Fix evernote backend - - * 2013-03-18 Matías Aguirre - Yahoo tests - - * 2013-03-18 Matías Aguirre - Bitbucket tests - - * 2013-03-18 Matías Aguirre - OAuth1 base tests - - * 2013-03-18 Matías Aguirre - Fix OAuth1 unauth_token check. Support access token method on OAuth1 - backends too. Fix test strategry method - - * 2013-03-17 Matías Aguirre - Move shared code to a method - - * 2013-03-17 Matías Aguirre - Simplify backend definition to avoid messing with sys.path on each test - - * 2013-03-17 Matías Aguirre - Moved backends tests to backends module - - * 2013-03-17 Matías Aguirre - Use python3 workaround utils - - * 2013-03-17 Matías Aguirre - Removed unused imports - - * 2013-03-17 Matías Aguirre - Stackoverflow tests - - * 2013-03-17 Matías Aguirre - Fix stackoverflow backend - - * 2013-03-17 Matías Aguirre - Easier way to override access token response processing on oauth2 backends - - * 2013-03-17 Matías Aguirre - Enable stackoverflow backend on example app - - * 2013-03-17 Matías Aguirre - Yandex tests - - * 2013-03-17 Matías Aguirre - Fix yandex backend - - * 2013-03-17 Matías Aguirre - Yammer tests - - * 2013-03-17 Matías Aguirre - Fix yammer backend - - * 2013-03-17 Matías Aguirre - Enable yammer backend in example app - - * 2013-03-17 Matías Aguirre - Stocktwits tests - - * 2013-03-17 Matías Aguirre - Fix stocktwits backend - - * 2013-03-17 Matías Aguirre - Stripe tests - - * 2013-03-17 Matías Aguirre - Fix stripe backend - - * 2013-03-17 Matías Aguirre - Soundcloud test - - * 2013-03-17 Matías Aguirre - Fix soundcloud backend - - * 2013-03-17 Matías Aguirre - Small code simplification in shopify backend - - * 2013-03-17 Matías Aguirre - Enable rdio backends in example app - - * 2013-03-17 Matías Aguirre - Fix Rdio backends - - * 2013-03-17 Matías Aguirre - Mixcloud tests - - * 2013-03-17 Matías Aguirre - Fix mixcloud backend - - * 2013-03-17 Matías Aguirre - Enable mixcloud backend into example app - - * 2013-03-17 Matías Aguirre - Fix and simplify mail.ru backend - - * 2013-03-17 Matías Aguirre - Live tests - - * 2013-03-17 Matías Aguirre - Fix Live backend - - * 2013-03-17 Matías Aguirre - Instagram tests - - * 2013-03-17 Matías Aguirre - Fix instagram backend - - * 2013-03-17 Matías Aguirre - Google oauth2 test - - * 2013-03-17 Matías Aguirre - Fix google oauth2 backend - - * 2013-03-17 Matías Aguirre - Foursquare test - - * 2013-03-17 Matías Aguirre - Rename douban oauth2 backend. Enable douban backend in example app - - * 2013-03-17 Matías Aguirre - Fixed foursquare backend - - * 2013-03-16 Matías Aguirre - Disqus tests - - * 2013-03-16 Matías Aguirre - Fix disqus backend - - * 2013-03-16 Matías Aguirre - Dailymotion test - - * 2013-03-16 Matías Aguirre - Fix error processing - - * 2013-03-16 Matías Aguirre - Fix dailymotion backend - - * 2013-03-16 Matías Aguirre - Behance tests case - - * 2013-03-16 Matías Aguirre - Fix behance backend - - * 2013-03-16 Matías Aguirre - Support backends that don't support state/redirect_state. Angel backend - test - - * 2013-03-16 Matías Aguirre - Allow POST method on access_token exchange. Fixes angel backend - - * 2013-03-16 Matías Aguirre - Facebook tests - - * 2013-03-16 Matías Aguirre - Update requests values properly - - * 2013-03-16 Matías Aguirre - Remove unittest call from github tests - - * 2013-03-16 Matías Aguirre - Requirements to run tests - - * 2013-03-16 Matías Aguirre - Reshape flask example app. Use filter_by instead of filter on disconnect - code - - * 2013-03-16 Matías Aguirre - Use declarative sqlalchemy base. Drop global update on init_social. Refs #1 - - * 2013-03-16 Matías Aguirre - Small fixes - - * 2013-03-16 Matías Aguirre - Initial testings - - * 2013-03-15 Matías Aguirre - Drop parse_qsl call - - * 2013-03-15 Matías Aguirre - Remove empty line - - * 2013-03-15 Matías Aguirre - Proper requirements for python3 and python2 - - * 2013-03-14 Matías Aguirre - Fix token comparision to avoid None == None case - - * 2013-03-14 Matías Aguirre - Improve refresh_token response processing - - * 2013-03-14 Matías Aguirre - Fix use of request on stackoverflow backend - - * 2013-03-13 Matías Aguirre - Fix google oauth1 - - * 2013-03-13 Matías Aguirre - Fix Python2 import, define json field in a compatible way with python - versions - - * 2013-03-13 Matías Aguirre - More Python3 support - - * 2013-03-13 Matías Aguirre - Django 1.5 and Python 3 support - - * 2013-03-12 Matías Aguirre - Port OAuth1 backends to oauthlib and requests-oauthlib - - * 2013-03-12 Matías Aguirre - Move views/routes code to common module - - * 2013-03-06 Matías Aguirre - Flag new associations in the pipeline - - * 2013-03-05 Matías Aguirre - Port from urllib/urllib2 urlopen to python-requests - - * 2013-03-03 Jannis Leidel - Fixed South introspection path to new module structure. - - * 2013-02-28 Matías Aguirre - Rename doc - - * 2013-02-28 Matías Aguirre - Add some note about porting DSA settings - - * 2013-02-28 Matías Aguirre - Mention mongoengine storage setting - - * 2013-02-27 Matías Aguirre - Port DSA evernote expire time normalization - - * 2013-02-27 Matías Aguirre - Link to project homepage and docs - - * 2013-02-27 Matías Aguirre - Fix long_description on setup.py file - - * 2013-02-27 Matías Aguirre - Remove frameworks classifier - - * 2013-02-27 Matías Aguirre - Extra bits for versioning - - * 2013-02-27 Matías Aguirre - Small requirements.txt for webpy example app - - * 2013-02-27 Matías Aguirre - Comment on how to run migrations - - * 2013-02-27 Matías Aguirre - Add default url prefix - - * 2013-02-27 Matías Aguirre - Fix URLs paths for django app, support setting url prefix (to ease demo - setup) - - * 2013-02-26 Matías Aguirre - Add fabfile to ignore list - - * 2013-02-26 Matías Aguirre - Mark dev version - - * 2013-02-26 Matías Aguirre - Small markup changes - - * 2013-02-26 Matías Aguirre - Porting from DSA docs - - * 2013-02-26 Matías Aguirre - Update django url docs - - * 2013-02-26 Matías Aguirre - Remove the obsolete BACKENDS attribute, simplify backends loading - - * 2013-02-26 Matías Aguirre - Links to mailing list and irc channel - - * 2013-02-26 Matías Aguirre - Improve disconnect process and example apps styles/disconnect triggering - - * 2013-02-26 Matías Aguirre - Webpy example improves - - * 2013-02-26 Matías Aguirre - Remove print statement - - * 2013-02-26 Matías Aguirre - Fix disconnect link, must be a POST action - - * 2013-02-26 Matías Aguirre - Flask app and example improves - - * 2013-02-26 Matías Aguirre - Django app and example improves - - * 2013-02-25 Matías Aguirre - Move code to avoid dependencies issues - - * 2013-02-25 Matías Aguirre - Basic site plus implementing backends docs - - * 2013-02-25 Matías Aguirre - Add URL attribute to open id classes to reduce methods overrides - - * 2013-02-24 Matías Aguirre - Docs markup improves - - * 2013-02-24 Matías Aguirre - pypi setup file - - * 2013-02-24 Matías Aguirre - Small docs changes. Versioning - - * 2013-02-24 Matías Aguirre - Docs - - * 2013-02-24 Matías Aguirre - Rename some settings, improve vkontakte backend code - - * 2013-02-24 Matías Aguirre - Four spaces indentation - - * 2013-02-23 Matías Aguirre - Added template filters to flask app - - * 2013-02-23 Matías Aguirre - Added context_processors for django app - - * 2013-02-23 Matías Aguirre - Remove HTML template, leave that to developers - - * 2013-02-23 Matías Aguirre - Ported rdio backend from DSA - - * 2013-02-23 Matías Aguirre - Port tumblr backend from DSA - - * 2013-02-23 Matías Aguirre - Port last douban changes from DSA - - * 2013-02-23 Matías Aguirre - Port fields stored in session from DSA - - * 2013-02-23 Matías Aguirre - Port slugify option from DSA - - * 2013-02-23 Matías Aguirre - Enforce POST method on disconnect views - - * 2013-02-23 Matías Aguirre - Ported SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL setting - - * 2013-02-22 Matías Aguirre - Move is_active check to a function - - * 2013-02-22 Matías Aguirre - Move is_authenticated check to a function - - * 2013-02-17 Matías Aguirre - Add django middleware - - * 2013-02-17 Matías Aguirre - Licence - - * 2013-02-17 Matías Aguirre - Remove unused var - - * 2013-02-17 Matías Aguirre - Stackoverflow backend - - * 2013-02-17 Matías Aguirre - Readability backend - - * 2013-02-17 Matías Aguirre - Rename to be consistent with backend name - - * 2013-02-17 Matías Aguirre - Steam backend - - * 2013-02-17 Matías Aguirre - Move methods - - * 2013-02-17 Matías Aguirre - Fix removeAssociation method - - * 2013-02-17 Matías Aguirre - Docstrings, methods movement, simple_user_exists rename to user_exists - - * 2013-02-17 Matías Aguirre - Fix setting retrieval and remove testing webpy view - - * 2013-02-17 Matías Aguirre - Move urls inside classes and fix a few on skyrock - - * 2013-01-29 Matías Aguirre - Webpy example app - - * 2013-01-29 Matías Aguirre - Webpy strategy and app - - * 2012-12-31 Matías Aguirre - Moved get_strategy to strategies/utils.py - - * 2012-12-30 Matías Aguirre - Cleanups - - * 2012-12-30 Matías Aguirre - Rename modules - - * 2012-12-29 Matías Aguirre - Add more examples links to flask example app - - * 2012-12-29 Matías Aguirre - Fix association/nonce filtering queries - - * 2012-12-29 Matías Aguirre - Fix absolute url building process - - * 2012-12-29 Matías Aguirre - Enable GET/POST in routes - - * 2012-12-29 Matías Aguirre - Set app in debug mode by default - - * 2012-12-29 Matías Aguirre - Flask example - - * 2012-12-29 Matías Aguirre - Flask strategy, sqlalchemy storage and flask app - - * 2012-12-29 Matías Aguirre - Clean old settings, move code to main utils - - * 2012-12-28 Matías Aguirre - Clean unused methods, move integrity error check to storage layer - - * 2012-12-27 Matías Aguirre - Move store out from strategies module - - * 2012-12-26 Matías Aguirre - Fixes and tests - - * 2012-12-25 Matías Aguirre - Filter models by user - - * 2012-12-25 Matías Aguirre - Rename example directory - - * 2012-12-25 Matías Aguirre - Filter models by user - - * 2012-12-25 Matías Aguirre - Port removeAssociation from django-social-auth - - * 2012-12-25 Matías Aguirre - Add setting shortcut to backends - - * 2012-12-25 Matías Aguirre - Odnoklassniki backend - - * 2012-12-25 Matías Aguirre - Remove property decorator. PEP8 - - * 2012-12-25 Matías Aguirre - Cookies handling - - * 2012-12-20 Matías Aguirre - Fix key - - * 2012-12-16 Matías Aguirre - Yandex backends - - * 2012-12-16 Matías Aguirre - LiveJournal backend - - * 2012-12-16 Matías Aguirre - Ported django-social-auth oauth backends - - * 2012-12-16 Matías Aguirre - Google App Engine backend - - * 2012-12-16 Matías Aguirre - Ported django-social-auth oauth2 backends - - * 2012-12-16 Matías Aguirre - Facebook backend - - * 2012-12-15 Matías Aguirre - Persona Auth - - * 2012-12-15 Matías Aguirre - Stripe OAuth2 backend - - * 2012-12-15 Matías Aguirre - Yahoo OpenId backend - - * 2012-12-15 Matías Aguirre - Twitter OAuth backend - - * 2012-12-15 Matías Aguirre - Google OpenId backend - - * 2012-12-15 Matías Aguirre - Google oauth backend - - * 2012-12-14 Matías Aguirre - Make openid work - - * 2012-12-14 Matías Aguirre - Simplify Strategy and Storage loading on django app - - * 2012-12-14 Matías Aguirre - Remove unneeded function - - * 2012-12-14 Matías Aguirre - Apps description on __init__.py files - - * 2012-12-14 Matías Aguirre - Restore import line - - * 2012-12-14 Matías Aguirre - Django example requirements.txt - - * 2012-12-14 Matías Aguirre - requirements.txt - - * 2012-12-14 Matías Aguirre - Move dj app views/utils/urls since they can be used by mongoengine too - - * 2012-12-14 Matías Aguirre - Initial google oauth2 backend and django example app - - * 2012-12-13 Matías Aguirre - Initial code for this lib (not working yet) - - * 2012-12-12 Matías Aguirre - Initial commit diff --git a/MANIFEST.in b/MANIFEST.in index d2fef433d..6a12ea00c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ global-include *.py -include *.txt Changelog LICENSE README.rst +include *.txt CHANGELOG.md LICENSE README.rst recursive-include docs *.rst recursive-include social/tests *.txt From bf1d48c8cc0e71d9aec89095d55b685e9c4713ca Mon Sep 17 00:00:00 2001 From: Mishbah Razzaque Date: Tue, 26 Jan 2016 17:04:19 +0000 Subject: [PATCH 746/890] =?UTF-8?q?Fixing=20ImportError:=20cannot=20import?= =?UTF-8?q?=20name=20=E2=80=98urlencode=E2=80=99=20in=20Python3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- social/backends/salesforce.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/social/backends/salesforce.py b/social/backends/salesforce.py index ccaca6302..066f072c8 100644 --- a/social/backends/salesforce.py +++ b/social/backends/salesforce.py @@ -1,6 +1,10 @@ -from urllib import urlencode from social.backends.oauth import BaseOAuth2 +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + class SalesforceOAuth2(BaseOAuth2): """Salesforce OAuth2 authentication backend""" From dad3e6e8d551dd221920ce36accf1afcd42aedf6 Mon Sep 17 00:00:00 2001 From: Mishbah Razzaque Date: Wed, 27 Jan 2016 14:42:09 +0000 Subject: [PATCH 747/890] Use builtin compat urlencode --- social/backends/salesforce.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/social/backends/salesforce.py b/social/backends/salesforce.py index 066f072c8..a77aa854a 100644 --- a/social/backends/salesforce.py +++ b/social/backends/salesforce.py @@ -1,9 +1,5 @@ from social.backends.oauth import BaseOAuth2 - -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode +from social.p3 import urlencode class SalesforceOAuth2(BaseOAuth2): From a21f0548bf2914c41880a1f417c58a4dc7e089bc Mon Sep 17 00:00:00 2001 From: strikki Date: Wed, 27 Jan 2016 18:20:02 +0300 Subject: [PATCH 748/890] Added optional 'include_email' query param for Twitter backend. --- docs/backends/twitter.rst | 5 + social/backends/twitter.py | 9 +- social/tests/backends/test_twitter.py | 132 ++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/docs/backends/twitter.rst b/docs/backends/twitter.rst index e40c41383..6fcedd811 100644 --- a/docs/backends/twitter.rst +++ b/docs/backends/twitter.rst @@ -20,6 +20,10 @@ To enable Twitter these two keys are needed. Further documentation at Client type instead of the Browser. Almost any dummy value will work if you plan some test. +- You can request user's Email address by setting (consult `Twitter verify credentials`_):: + + SOCIAL_AUTH_TWITTER_INCLUDE_EMAIL = True + Twitter usually fails with a 401 error when trying to call the request-token URL, this is usually caused by server datetime errors (check miscellaneous section). Installing ``ntp`` and syncing the server date with some pool does @@ -27,3 +31,4 @@ the trick. .. _Twitter development resources: http://dev.twitter.com/pages/auth .. _Twitter App Creation: http://twitter.com/apps/new +.. _Twitter verify credentials: https://dev.twitter.com/rest/reference/get/account/verify_credentials \ No newline at end of file diff --git a/social/backends/twitter.py b/social/backends/twitter.py index c41f15ab1..20d06fb7d 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -27,14 +27,17 @@ def get_user_details(self, response): """Return user details from Twitter account""" fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': response['screen_name'], - 'email': '', # not supplied + 'email': response.get('email', ''), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Return user data provided""" + url = 'https://api.twitter.com/1.1/account/verify_credentials.json' + if self.setting('INCLUDE_EMAIL', False): + url += '?include_email=true' return self.get_json( - 'https://api.twitter.com/1.1/account/verify_credentials.json', - auth=self.oauth_auth(access_token) + url, + auth=self.oauth_auth(access_token) ) diff --git a/social/tests/backends/test_twitter.py b/social/tests/backends/test_twitter.py index 430023776..d4eaef8e4 100644 --- a/social/tests/backends/test_twitter.py +++ b/social/tests/backends/test_twitter.py @@ -129,3 +129,135 @@ def test_login(self): def test_partial_pipeline(self): self.do_partial_pipeline() + + +class TwitterOAuth1IncludeEmailTest(OAuth1Test): + backend_path = 'social.backends.twitter.TwitterOAuth' + user_data_url = 'https://api.twitter.com/1.1/account/' \ + 'verify_credentials.json?include_email=true' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + request_token_body = urlencode({ + 'oauth_token_secret': 'foobar-secret', + 'oauth_token': 'foobar', + 'oauth_callback_confirmed': 'true' + }) + user_data_body = json.dumps({ + 'follow_request_sent': False, + 'profile_use_background_image': True, + 'id': 10101010, + 'description': 'Foo bar baz qux', + 'verified': False, + 'entities': { + 'description': { + 'urls': [] + } + }, + 'profile_image_url_https': 'https://twimg0-a.akamaihd.net/' + 'profile_images/532018826/' + 'n587119531_1939735_9305_normal.jpg', + 'profile_sidebar_fill_color': '252429', + 'profile_text_color': '666666', + 'followers_count': 77, + 'profile_sidebar_border_color': '181A1E', + 'location': 'Fooland', + 'default_profile_image': False, + 'listed_count': 4, + 'status': { + 'favorited': False, + 'contributors': None, + 'retweeted_status': { + 'favorited': False, + 'contributors': None, + 'truncated': False, + 'source': 'web', + 'text': '"Foo foo foo foo', + 'created_at': 'Fri Dec 21 18:12:00 +0000 2012', + 'retweeted': True, + 'in_reply_to_status_id': None, + 'coordinates': None, + 'id': 101010101010101010, + 'entities': { + 'user_mentions': [], + 'hashtags': [], + 'urls': [] + }, + 'in_reply_to_status_id_str': None, + 'place': None, + 'id_str': '101010101010101010', + 'in_reply_to_screen_name': None, + 'retweet_count': 8, + 'geo': None, + 'in_reply_to_user_id_str': None, + 'in_reply_to_user_id': None + }, + 'truncated': False, + 'source': 'web', + 'text': 'RT @foo: "Foo foo foo foo', + 'created_at': 'Fri Dec 21 18:24:10 +0000 2012', + 'retweeted': True, + 'in_reply_to_status_id': None, + 'coordinates': None, + 'id': 101010101010101010, + 'entities': { + 'user_mentions': [{ + 'indices': [3, 10], + 'id': 10101010, + 'screen_name': 'foo', + 'id_str': '10101010', + 'name': 'Foo' + }], + 'hashtags': [], + 'urls': [] + }, + 'in_reply_to_status_id_str': None, + 'place': None, + 'id_str': '101010101010101010', + 'in_reply_to_screen_name': None, + 'retweet_count': 8, + 'geo': None, + 'in_reply_to_user_id_str': None, + 'in_reply_to_user_id': None + }, + 'utc_offset': -10800, + 'statuses_count': 191, + 'profile_background_color': '1A1B1F', + 'friends_count': 151, + 'profile_background_image_url_https': 'https://twimg0-a.akamaihd.net/' + 'images/themes/theme9/bg.gif', + 'profile_link_color': '2FC2EF', + 'profile_image_url': 'http://a0.twimg.com/profile_images/532018826/' + 'n587119531_1939735_9305_normal.jpg', + 'is_translator': False, + 'geo_enabled': False, + 'id_str': '74313638', + 'profile_background_image_url': 'http://a0.twimg.com/images/themes/' + 'theme9/bg.gif', + 'screen_name': 'foobar', + 'lang': 'en', + 'profile_background_tile': False, + 'favourites_count': 2, + 'name': 'Foo', + 'notifications': False, + 'url': None, + 'created_at': 'Tue Sep 15 00:26:17 +0000 2009', + 'contributors_enabled': False, + 'time_zone': 'Buenos Aires', + 'protected': False, + 'default_profile': False, + 'following': False, + 'email': 'foo@bar.bas' + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_TWITTER_INCLUDE_EMAIL': True + }) + user = self.do_login() + self.assertEquals(user.email, 'foo@bar.bas') + + def test_partial_pipeline(self): + self.do_partial_pipeline() From b015d6c40f815237402d1e5f16340e5ff5cfe4f0 Mon Sep 17 00:00:00 2001 From: Igor Serko Date: Tue, 2 Feb 2016 11:40:28 +0000 Subject: [PATCH 749/890] add github enterprise docs on how to specify the API URL --- docs/backends/github_enterprise.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/backends/github_enterprise.rst b/docs/backends/github_enterprise.rst index 5d59cc907..29f5d6d83 100644 --- a/docs/backends/github_enterprise.rst +++ b/docs/backends/github_enterprise.rst @@ -9,7 +9,11 @@ GitHub Enterprise works similar to regular Github, which is in turn based on Fac set the callback URL to ``http://example.com/complete/github/`` replacing ``example.com`` with your domain. -- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings:: +- Set the API URL for your Github Enterprise appliance: + + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL = 'https://git.example.com/api/v3/' + +- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings: SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY = '' SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET = '' From 6a6d5c823a946b581ea7f294cdc3d55e8bbb18d1 Mon Sep 17 00:00:00 2001 From: vanadium23 Date: Wed, 3 Feb 2016 08:04:29 +0300 Subject: [PATCH 750/890] [Fix] update odnoklasniki docs to new domain ok --- docs/backends/odnoklassnikiru.rst | 8 ++++---- social/backends/odnoklassniki.py | 13 ++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/backends/odnoklassnikiru.rst b/docs/backends/odnoklassnikiru.rst index 8eecb7591..772997566 100644 --- a/docs/backends/odnoklassnikiru.rst +++ b/docs/backends/odnoklassnikiru.rst @@ -50,7 +50,7 @@ You may also use:: Defaults to empty tuple, for the list of available fields see `Documentation on user.getInfo`_ -.. _OAuth registration form: http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=13992188 -.. _Rules for application developers: http://dev.odnoklassniki.ru/wiki/display/ok/Odnoklassniki.ru+Third+Party+Platform -.. _Developers registration form: http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=5668937 -.. _Documentation on user.getInfo: http://dev.odnoklassniki.ru/wiki/display/ok/REST+API+-+users.getInfo +.. _OAuth registration form: https://apiok.ru/wiki/pages/viewpage.action?pageId=42476652 +.. _Rules for application developers: https://apiok.ru/wiki/display/ok/Odnoklassniki.ru+Third+Party+Platform +.. _Developers registration form: https://apiok.ru/wiki/pages/viewpage.action?pageId=5668937 +.. _Documentation on user.getInfo: https://apiok.ru/wiki/display/ok/REST+API+-+users.getInfo diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 1a7cf267c..4981b0339 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -16,8 +16,8 @@ class OdnoklassnikiOAuth2(BaseOAuth2): ID_KEY = 'uid' ACCESS_TOKEN_METHOD = 'POST' SCOPE_SEPARATOR = ';' - AUTHORIZATION_URL = 'http://www.odnoklassniki.ru/oauth/authorize' - ACCESS_TOKEN_URL = 'http://api.odnoklassniki.ru/oauth/token.do' + AUTHORIZATION_URL = 'https://connect.ok.ru/oauth/authorize' + ACCESS_TOKEN_URL = 'https://api.ok.ru/oauth/token.do' EXTRA_DATA = [('refresh_token', 'refresh_token'), ('expires_in', 'expires')] @@ -41,7 +41,7 @@ def user_data(self, access_token, *args, **kwargs): data = {'access_token': access_token, 'method': 'users.getCurrentUser'} key, secret = self.get_key_and_secret() public_key = self.setting('PUBLIC_NAME') - return odnoklassniki_api(self, data, 'http://api.odnoklassniki.ru/', + return odnoklassniki_api(self, data, 'https://api.ok.ru/', public_key, secret, 'oauth') @@ -123,7 +123,7 @@ def odnoklassniki_oauth_sig(data, client_secret): """ Calculates signature of request data access_token value must be included Algorithm is described at - http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=12878032, + https://apiok.ru/wiki/pages/viewpage.action?pageId=12878032, search for "little bit different way" """ suffix = md5( @@ -139,8 +139,7 @@ def odnoklassniki_oauth_sig(data, client_secret): def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): """ Calculates signature as described at: - http://dev.odnoklassniki.ru/wiki/display/ok/ - Authentication+and+Authorization + https://apiok.ru/wiki/display/ok/Authentication+and+Authorization If API method requires session context, request is signed with session secret key. Otherwise it is signed with application secret key """ @@ -154,7 +153,7 @@ def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): def odnoklassniki_api(backend, data, api_url, public_key, client_secret, request_type='oauth'): """Calls Odnoklassniki REST API method - http://dev.odnoklassniki.ru/wiki/display/ok/Odnoklassniki+Rest+API""" + https://apiok.ru/wiki/display/ok/Odnoklassniki+Rest+API""" data.update({ 'application_key': public_key, 'format': 'JSON' From f504a41cdd642751d0ca638c35b5ebdea2cda8cf Mon Sep 17 00:00:00 2001 From: khamaileon Date: Mon, 15 Feb 2016 17:49:56 +0100 Subject: [PATCH 751/890] Add unit tests for Spotify backend --- social/backends/spotify.py | 3 ++- social/tests/backends/test_spotify.py | 34 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 social/tests/backends/test_spotify.py diff --git a/social/backends/spotify.py b/social/backends/spotify.py index e8ab88ed7..f2c64d7d8 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -9,12 +9,13 @@ class SpotifyOAuth2(BaseOAuth2): + """Spotify OAuth2 authentication backend""" name = 'spotify' - SCOPE_SEPARATOR = ' ' ID_KEY = 'id' AUTHORIZATION_URL = 'https://accounts.spotify.com/authorize' ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token' ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ' ' REDIRECT_STATE = False EXTRA_DATA = [ ('refresh_token', 'refresh_token'), diff --git a/social/tests/backends/test_spotify.py b/social/tests/backends/test_spotify.py new file mode 100644 index 000000000..73abbc4d5 --- /dev/null +++ b/social/tests/backends/test_spotify.py @@ -0,0 +1,34 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class SpotifyOAuth2Test(OAuth2Test): + backend_path = 'social.backends.spotify.SpotifyOAuth2' + user_data_url = 'https://api.spotify.com/v1/me' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'display_name': None, + 'external_urls': { + 'spotify': 'https://open.spotify.com/user/foobar' + }, + 'followers': { + 'href': None, + 'total': 0 + }, + 'href': 'https://api.spotify.com/v1/users/foobar', + 'id': 'foobar', + 'images': [], + 'type': 'user', + 'uri': 'spotify:user:foobar' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 7fa77755f824117fd9bebaadd779265399483c54 Mon Sep 17 00:00:00 2001 From: Victor Marques Date: Tue, 16 Feb 2016 01:21:20 -0300 Subject: [PATCH 752/890] Fix misspelled backend name --- docs/backends/instagram.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/instagram.rst b/docs/backends/instagram.rst index ccbcf1d42..dbaf86e1f 100644 --- a/docs/backends/instagram.rst +++ b/docs/backends/instagram.rst @@ -9,7 +9,7 @@ Instagram uses OAuth v2 for Authentication. AUTHENTICATION_SETTINGS = ( ... - 'social.backends.insagram.InstagramOAuth2', + 'social.backends.instagram.InstagramOAuth2', ... ) From 57e0d96aa89e27b8f657a09fc6814f7d5316d52d Mon Sep 17 00:00:00 2001 From: khamaileon Date: Tue, 16 Feb 2016 17:56:51 +0100 Subject: [PATCH 753/890] Add a backend for Deezer music service --- docs/backends/index.rst | 1 + docs/intro.rst | 2 + examples/django_example/example/settings.py | 1 + social/backends/deezer.py | 49 +++++++++++++++++++++ social/tests/backends/test_deezer.py | 36 +++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 social/backends/deezer.py create mode 100644 social/tests/backends/test_deezer.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 3d6b7e613..c38b4ccbb 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -63,6 +63,7 @@ Social backends coinbase coursera dailymotion + deezer digitalocean disqus docker diff --git a/docs/intro.rst b/docs/intro.rst index b138974dc..a52d568b7 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -40,6 +40,7 @@ or extend current one): * Bitbucket_ OAuth1 * Box_ OAuth2 * Dailymotion_ OAuth2 + * Deezer_ OAuth2 * Disqus_ OAuth2 * Douban_ OAuth1 and OAuth2 * Dropbox_ OAuth1 @@ -120,6 +121,7 @@ section. .. _Bitbucket: https://bitbucket.org .. _Box: https://www.box.com .. _Dailymotion: https://dailymotion.com +.. _Deezer: https://www.deezer.com .. _Disqus: https://disqus.com .. _Douban: http://www.douban.com .. _Dropbox: https://dropbox.com diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 58ab9c720..6e6e98bb2 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -132,6 +132,7 @@ 'social.backends.coinbase.CoinbaseOAuth2', 'social.backends.coursera.CourseraOAuth2', 'social.backends.dailymotion.DailymotionOAuth2', + 'social.backends.deezer.DeezerOAuth2', 'social.backends.disqus.DisqusOAuth2', 'social.backends.douban.DoubanOAuth2', 'social.backends.dropbox.DropboxOAuth', diff --git a/social/backends/deezer.py b/social/backends/deezer.py new file mode 100644 index 000000000..c7c18a267 --- /dev/null +++ b/social/backends/deezer.py @@ -0,0 +1,49 @@ +""" +Deezer backend, docs at: + https://developers.deezer.com/api/oauth + https://developers.deezer.com/api/permissions +""" +from urllib.parse import parse_qsl + +from social.backends.oauth import BaseOAuth2 + + +class DeezerOAuth2(BaseOAuth2): + """Deezer OAuth2 authentication backend""" + name = 'deezer' + ID_KEY = 'name' + AUTHORIZATION_URL = 'https://connect.deezer.com/oauth/auth.php' + ACCESS_TOKEN_URL = 'https://connect.deezer.com/oauth/access_token.php' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' + REDIRECT_STATE = False + + def auth_complete_params(self, state=None): + client_id, client_secret = self.get_key_and_secret() + return { + 'app_id': client_id, + 'secret': client_secret, + 'code': self.data.get('code') + } + + def request_access_token(self, *args, **kwargs): + response = self.request(*args, **kwargs) + return dict(parse_qsl(response.text)) + + def get_user_details(self, response): + """Return user details from Deezer account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('firstname'), + last_name=response.get('lastname') + ) + return {'username': response.get('name'), + 'email': response.get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('http://api.deezer.com/user/me', params={ + 'access_token': access_token + }) diff --git a/social/tests/backends/test_deezer.py b/social/tests/backends/test_deezer.py new file mode 100644 index 000000000..3c74caea1 --- /dev/null +++ b/social/tests/backends/test_deezer.py @@ -0,0 +1,36 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class DeezerOAuth2Test(OAuth2Test): + backend_path = 'social.backends.deezer.DeezerOAuth2' + user_data_url = 'http://api.deezer.com/user/me' + expected_username = 'foobar' + access_token_body = 'access_token=foobar&expires=0' + user_data_body = json.dumps({ + 'id': '1', + 'name': 'foobar', + 'lastname': '', + 'firstname': '', + 'status': 0, + 'birthday': '1970-01-01', + 'inscription_date': '2015-01-01', + 'gender': 'M', + 'link': 'https://www.deezer.com/profile/1', + 'picture': 'https://api.deezer.com/user/1/image', + 'picture_small': 'https://cdns-images.dzcdn.net/images/user//56x56-000000-80-0-0.jpg', + 'picture_medium': 'https://cdns-images.dzcdn.net/images/user//250x250-000000-80-0-0.jpg', + 'picture_big': 'https://cdns-images.dzcdn.net/images/user//500x500-000000-80-0-0.jpg', + 'country': 'FR', + 'lang': 'FR', + 'is_kid': False, + 'tracklist': 'https://api.deezer.com/user/1/flow', + 'type': 'user' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From b5c4c7b60dbbe0e6246ebf5f80e7ccfb6f08070c Mon Sep 17 00:00:00 2001 From: khamaileon Date: Tue, 16 Feb 2016 18:11:31 +0100 Subject: [PATCH 754/890] Fix python 2 compatibility issue --- social/backends/deezer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/deezer.py b/social/backends/deezer.py index c7c18a267..947afc065 100644 --- a/social/backends/deezer.py +++ b/social/backends/deezer.py @@ -3,7 +3,7 @@ https://developers.deezer.com/api/oauth https://developers.deezer.com/api/permissions """ -from urllib.parse import parse_qsl +from six.moves.urllib.parse import parse_qsl from social.backends.oauth import BaseOAuth2 From 8ccc7326fb568042c22d539fad8e50bb7d73b1f1 Mon Sep 17 00:00:00 2001 From: hellvix Date: Tue, 23 Feb 2016 14:19:26 -0300 Subject: [PATCH 755/890] Update base.py Fixes exception 'str' object has no attribute 'update' when user profile is updated. --- social/storage/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/storage/base.py b/social/storage/base.py index 1964bc1a2..f5f57af2c 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -81,7 +81,7 @@ def expiration_datetime(self): def set_extra_data(self, extra_data=None): if extra_data and self.extra_data != extra_data: - if self.extra_data: + if self.extra_data and not isinstance(self.extra_data, str): self.extra_data.update(extra_data) else: self.extra_data = extra_data From f3546548541b2a1f0d2b5b13f63e738f959ead7f Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Sat, 27 Feb 2016 14:32:15 -0300 Subject: [PATCH 756/890] Fix xgettext warning due to unknown encoding --- social/backends/weibo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/weibo.py b/social/backends/weibo.py index 25b60a94f..6cc7844c3 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -1,4 +1,4 @@ -# coding:utf8 +# coding:utf-8 # author:hepochen@gmail.com https://github.com/hepochen """ Weibo OAuth2 backend, docs at: From fbaa002d8970c05ff6f3bf4e2ffeb8e912f5cd7d Mon Sep 17 00:00:00 2001 From: "Alain St. Pierre" Date: Wed, 2 Mar 2016 17:12:32 -0800 Subject: [PATCH 757/890] added tests --- docs/backends/index.rst | 1 + social/tests/backends/test_arcgis.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 social/tests/backends/test_arcgis.py diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 3d6b7e613..ba4de6eb9 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -51,6 +51,7 @@ Social backends angel aol appsfuel + arcgis azuread battlenet beats diff --git a/social/tests/backends/test_arcgis.py b/social/tests/backends/test_arcgis.py new file mode 100644 index 000000000..39d4b6e84 --- /dev/null +++ b/social/tests/backends/test_arcgis.py @@ -0,0 +1,27 @@ +import json +from social.tests.backends.oauth import OAuth2Test + + +class ArcGISOAuth2Test(OAuth2Test): + user_data_url = 'https://www.arcgis.com/sharing/rest/community/self' + backend_path = 'social.backends.arcgis.ArcGISOAuth2' + expected_username = 'gis@rocks.com' + + user_data_body = json.dumps({ + "first_name": "Gis", + "last_name": "Rocks", + "email": "gis@rocks.com", + "fullName": "Gis Rocks", + "username": "gis@rocks.com" + }) + + access_token_body = json.dumps({ + "access_token": "CM-gcB85taGhRmoI7l3PSGaXUNsaLkTg-dHH7XtA9DnlinPYKBBrIvFzhd1JtDhh7hEwSv_6eLLcLtUqe3gD6i1yaYYFpUQJwy8KEujke5AE87tP9XIoMtp4_l320pUL", + "expires_in": 86400 + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From f9e36da2891a84c67987cdfa8462869ae87458a1 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 14 Mar 2016 18:06:43 +0000 Subject: [PATCH 758/890] Do not instantiate Logger directly https://docs.python.org/2/library/logging.html#logger-objects --- social/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/utils.py b/social/utils.py index c2db351af..0a473af15 100644 --- a/social/utils.py +++ b/social/utils.py @@ -19,7 +19,7 @@ SETTING_PREFIX = 'SOCIAL_AUTH' -social_logger = logging.Logger('social') +social_logger = logging.getLogger('social') class SSLHttpAdapter(HTTPAdapter): From 9c0a77f7f433ee9d989a61bb670af002abf2a507 Mon Sep 17 00:00:00 2001 From: Chronial Date: Fri, 18 Mar 2016 16:09:05 +0100 Subject: [PATCH 759/890] Fix typo in comment --- social/pipeline/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/pipeline/__init__.py b/social/pipeline/__init__.py index 132a590f7..f87242d5a 100644 --- a/social/pipeline/__init__.py +++ b/social/pipeline/__init__.py @@ -10,7 +10,7 @@ 'social.pipeline.social_auth.social_uid', # Verifies that the current auth process is valid within the current - # project, this is were emails and domains whitelists are applied (if + # project, this is where emails and domains whitelists are applied (if # defined). 'social.pipeline.social_auth.auth_allowed', From 102262a5d54010e940444c7e48188b2cbb9150e0 Mon Sep 17 00:00:00 2001 From: Chronial Date: Fri, 18 Mar 2016 16:10:50 +0100 Subject: [PATCH 760/890] Fix typo in documentation --- docs/pipeline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index 715abf48e..bd6566047 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -41,7 +41,7 @@ The default pipeline is composed by:: 'social.pipeline.social_auth.social_uid', # Verifies that the current auth process is valid within the current - # project, this is were emails and domains whitelists are applied (if + # project, this is where emails and domains whitelists are applied (if # defined). 'social.pipeline.social_auth.auth_allowed', From 482b8b1f0589465f0c84555276840db3e7cacfa9 Mon Sep 17 00:00:00 2001 From: Fabian Alknes Date: Tue, 19 Jan 2016 15:26:42 +0100 Subject: [PATCH 761/890] Fix wrong evaluation of boolean kwargs --- social/pipeline/user.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index 38784cad6..a46fc3e63 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -59,9 +59,8 @@ def create_user(strategy, details, user=None, *args, **kwargs): if user: return {'is_new': False} - fields = dict((name, kwargs.get(name) or details.get(name)) - for name in strategy.setting('USER_FIELDS', - USER_FIELDS)) + fields = dict((name, kwargs.get(name, details.get(name))) + for name in strategy.setting('USER_FIELDS', USER_FIELDS)) if not fields: return From e9b56d32c6379e271ae67b9e4d75301ab6594f61 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Sat, 5 Mar 2016 23:45:13 +0100 Subject: [PATCH 762/890] Fix settings not being used in migrations Refs #777. --- .../default/migrations/0001_initial.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index e9960fb88..133e5dd62 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -7,15 +7,18 @@ import social.storage.django_orm from social.utils import setting_name -user_model = getattr(settings, setting_name('USER_MODEL'), None) or \ - getattr(settings, 'AUTH_USER_MODEL', None) or \ - 'auth.User' +USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ + getattr(settings, 'AUTH_USER_MODEL', None) or 'auth.User' +UID_LENGTH = getattr(settings, setting_name('UID_LENGTH'), 255) +NONCE_SERVER_URL_LENGTH = getattr(settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255) +ASSOCIATION_SERVER_URL_LENGTH = getattr(settings, setting_name('ASSOCIATION_SERVER_URL_LENGTH'), 255) +ASSOCIATION_HANDLE_LENGTH = getattr(settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255) class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(user_model), + migrations.swappable_dependency(USER_MODEL), ] operations = [ @@ -25,8 +28,8 @@ class Migration(migrations.Migration): ('id', models.AutoField( verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('server_url', models.CharField(max_length=255)), - ('handle', models.CharField(max_length=255)), + ('server_url', models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH)), + ('handle', models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH)), ('secret', models.CharField(max_length=255)), ('issued', models.IntegerField()), ('lifetime', models.IntegerField()), @@ -60,7 +63,7 @@ class Migration(migrations.Migration): ('id', models.AutoField( verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('server_url', models.CharField(max_length=255)), + ('server_url', models.CharField(max_length=NONCE_SERVER_URL_LENGTH)), ('timestamp', models.IntegerField()), ('salt', models.CharField(max_length=65)), ], @@ -76,11 +79,11 @@ class Migration(migrations.Migration): verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('provider', models.CharField(max_length=32)), - ('uid', models.CharField(max_length=255)), + ('uid', models.CharField(max_length=UID_LENGTH)), ('extra_data', social.apps.django_app.default.fields.JSONField( default='{}')), ('user', models.ForeignKey( - related_name='social_auth', to=user_model)), + related_name='social_auth', to=USER_MODEL)), ], options={ 'db_table': 'social_auth_usersocialauth', @@ -89,14 +92,14 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='usersocialauth', - unique_together=set([('provider', 'uid')]), + unique_together={('provider', 'uid')}, ), migrations.AlterUniqueTogether( name='code', - unique_together=set([('email', 'code')]), + unique_together={('email', 'code')}, ), migrations.AlterUniqueTogether( name='nonce', - unique_together=set([('server_url', 'timestamp', 'salt')]), + unique_together={('server_url', 'timestamp', 'salt')}, ), ] From bedef0ccaece7f69afda59f41471b5bda3388fe6 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Sun, 6 Mar 2016 00:31:06 +0100 Subject: [PATCH 763/890] Add SOCIAL_AUTH_EMAIL_LENGTH setting --- .../default/migrations/0003_alter_email_max_length.py | 8 ++++++-- social/apps/django_app/default/models.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py index e9cb54043..882c3112e 100644 --- a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py +++ b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.conf import settings from django.db import models, migrations +from social.utils import setting_name + +EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254) -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ ('default', '0002_add_related_name'), ] @@ -14,6 +18,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='code', name='email', - field=models.EmailField(max_length=254), + field=models.EmailField(max_length=EMAIL_LENGTH), ), ] diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index 16cb3d130..9f18529bb 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -19,6 +19,7 @@ getattr(settings, 'AUTH_USER_MODEL', None) or \ 'auth.User' UID_LENGTH = getattr(settings, setting_name('UID_LENGTH'), 255) +EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254) NONCE_SERVER_URL_LENGTH = getattr( settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255) ASSOCIATION_SERVER_URL_LENGTH = getattr( @@ -98,7 +99,7 @@ class Meta: class Code(models.Model, DjangoCodeMixin): - email = models.EmailField(max_length=254) + email = models.EmailField(max_length=EMAIL_LENGTH) code = models.CharField(max_length=32, db_index=True) verified = models.BooleanField(default=False) From 96fc4d1e2bb63fc0a18e9f3416655d81b119d20b Mon Sep 17 00:00:00 2001 From: hellvix Date: Tue, 23 Feb 2016 14:24:07 -0300 Subject: [PATCH 764/890] Update twitter.py Applications submitted to Twitter's privileged list can now obtain user e-mail address. --- social/backends/twitter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index c41f15ab1..9058bcfa0 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -27,7 +27,7 @@ def get_user_details(self, response): """Return user details from Twitter account""" fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': response['screen_name'], - 'email': '', # not supplied + 'email': response.get('email'), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} @@ -35,6 +35,6 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( - 'https://api.twitter.com/1.1/account/verify_credentials.json', + 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', auth=self.oauth_auth(access_token) ) From 23555a6b3ab58ffab2ecf7957cf715fb3ac49d72 Mon Sep 17 00:00:00 2001 From: hellvix Date: Fri, 11 Mar 2016 22:03:09 +0100 Subject: [PATCH 765/890] Update twitter.py Changed .get() method. Added default value. --- social/backends/twitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index 9058bcfa0..b07257bf8 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -27,7 +27,7 @@ def get_user_details(self, response): """Return user details from Twitter account""" fullname, first_name, last_name = self.get_user_names(response['name']) return {'username': response['screen_name'], - 'email': response.get('email'), + 'email': response.get('email', ''), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} From b8ee7d613bab8826751db0175f3351e861d2064c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 26 Mar 2016 03:39:46 -0300 Subject: [PATCH 766/890] Move querystring to parameters --- social/backends/twitter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/twitter.py b/social/backends/twitter.py index b07257bf8..7ccb7d2cf 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -35,6 +35,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( - 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', + 'https://api.twitter.com/1.1/account/verify_credentials.json', + params={'include_email': 'true'}, auth=self.oauth_auth(access_token) ) From e61ea1056292abf98cbf310d3c3ff3800c5e7c1f Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Tue, 16 Feb 2016 11:06:00 +0100 Subject: [PATCH 767/890] Add itembase backend --- social/backends/itembase.py | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 social/backends/itembase.py diff --git a/social/backends/itembase.py b/social/backends/itembase.py new file mode 100644 index 000000000..238db7320 --- /dev/null +++ b/social/backends/itembase.py @@ -0,0 +1,81 @@ +import time + +from social.backends.oauth import BaseOAuth2 +from social.utils import handle_http_errors + + +class ItembaseOAuth2(BaseOAuth2): + name = 'itembase' + ID_KEY = 'uuid' + AUTHORIZATION_URL = 'https://accounts.itembase.com/oauth/v2/auth' + ACCESS_TOKEN_URL = 'https://accounts.itembase.com/oauth/v2/token' + USER_DETAILS_URL = 'https://users.itembase.com/v1/me' + ACTIVATION_ENDPOINT = 'https://solutionservice.itembase.com/activate' + DEFAULT_SCOPE = ['user.minimal'] + EXTRA_DATA = [ + ('access_token', 'access_token'), + ('token_type', 'token_type'), + ('refresh_token', 'refresh_token'), + ('expires_in', 'expires_in'), # seconds to expiration + ('expires', 'expires'), # expiration timestamp in UTC + ('uuid', 'uuid'), + ('username', 'username'), + ('email', 'email'), + ('first_name', 'first_name'), + ('middle_name', 'middle_name'), + ('last_name', 'last_name'), + ('name_format', 'name_format'), + ('locale', 'locale'), + ('preferred_currency', 'preferred_currency'), + ] + + def add_expires(self, data): + data['expires'] = int(time.time()) + data.get('expires_in', 0) + return data + + def extra_data(self, user, uid, response, details=None, *args, **kwargs): + data = BaseOAuth2.extra_data(self, user, uid, response, details=details, *args, **kwargs) + return self.add_expires(data) + + def process_refresh_token_response(self, response, *args, **kwargs): + data = BaseOAuth2.process_refresh_token_response(self, response, *args, **kwargs) + return self.add_expires(data) + + def get_user_details(self, response): + """Return user details from Itembase account""" + return response + + def user_data(self, access_token, *args, **kwargs): + return self.get_json(self.USER_DETAILS_URL, + headers={'Authorization': 'Bearer {0}'.format(access_token)}) + + def activation_data(self, response): + # returns activation_data dict with activation_url inside + # see http://developers.itembase.com/authentication/activation + return self.get_json(self.ACTIVATION_ENDPOINT, + headers={'Authorization': 'Bearer {0}'.format(response['access_token'])}) + + @handle_http_errors + def auth_complete(self, *args, **kwargs): + """Completes login process, must return user instance""" + state = self.validate_state() + self.process_error(self.data) + # itembase needs GET request with params instead of just data + response = self.request_access_token( + self.access_token_url(), + params=self.auth_complete_params(state), + headers=self.auth_headers(), + auth=self.auth_complete_credentials(), + method=self.ACCESS_TOKEN_METHOD + ) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) + + +class ItembaseOAuth2Sandbox(ItembaseOAuth2): + name = 'itembase-sandbox' + AUTHORIZATION_URL = 'http://sandbox.accounts.itembase.io/oauth/v2/auth' + ACCESS_TOKEN_URL = 'http://sandbox.accounts.itembase.io/oauth/v2/token' + USER_DETAILS_URL = 'http://sandbox.users.itembase.io/v1/me' + ACTIVATION_ENDPOINT = 'http://sandbox.solutionservice.itembase.io/activate' From aeaa946c325ab71d09eda0a2c9400c116e210f22 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Tue, 16 Feb 2016 11:18:37 +0100 Subject: [PATCH 768/890] Add itembase docs --- docs/backends/itembase.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/backends/itembase.rst diff --git a/docs/backends/itembase.rst b/docs/backends/itembase.rst new file mode 100644 index 000000000..dcaa4917b --- /dev/null +++ b/docs/backends/itembase.rst @@ -0,0 +1,34 @@ +Itembase +========= + +Itembase uses OAuth2 for authentication. + +- Register a new application for the `Itembase API`_, and + +- Add itembase live backend and/or sandbox backend to ``AUTHENTICATION_SETTINGS``:: + + AUTHENTICATION_SETTINGS = ( + ... + 'social.backends.itembase.ItembaseOAuth2', + 'social.backends.itembase.ItembaseOAuth2Sandbox', + ... + ) + +- fill ``Client Id`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_ITEMBASE_KEY = '' + SOCIAL_AUTH_ITEMBASE_SECRET = '' + + SOCIAL_AUTH_ITEMBASE_SANDBOX_KEY = '' + SOCIAL_AUTH_ITEMBASE_SANDBOX_SECRET = '' + + +- extra scopes can be defined by using:: + + SOCIAL_AUTH_ITEMBASE_SCOPE = ['connection.transaction', + 'connection.product', + 'connection.profile', + 'connection.buyer'] + SOCIAL_AUTH_ITEMBASE_SANDBOX_SCOPE = SOCIAL_AUTH_ITEMBASE_SCOPE + +.. _Itembase API: http://developers.itembase.com/authentication/index From 817d4de6ccc38ff33e22ba06368e4d3c665efda7 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Tue, 16 Feb 2016 11:24:32 +0100 Subject: [PATCH 769/890] Update docs --- docs/backends/itembase.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/backends/itembase.rst b/docs/backends/itembase.rst index dcaa4917b..075a335fc 100644 --- a/docs/backends/itembase.rst +++ b/docs/backends/itembase.rst @@ -5,9 +5,9 @@ Itembase uses OAuth2 for authentication. - Register a new application for the `Itembase API`_, and -- Add itembase live backend and/or sandbox backend to ``AUTHENTICATION_SETTINGS``:: +- Add itembase live backend and/or sandbox backend to ``AUTHENTICATION_BACKENDS``:: - AUTHENTICATION_SETTINGS = ( + AUTHENTICATION_BACKENDS = ( ... 'social.backends.itembase.ItembaseOAuth2', 'social.backends.itembase.ItembaseOAuth2Sandbox', @@ -26,9 +26,9 @@ Itembase uses OAuth2 for authentication. - extra scopes can be defined by using:: SOCIAL_AUTH_ITEMBASE_SCOPE = ['connection.transaction', - 'connection.product', - 'connection.profile', - 'connection.buyer'] + 'connection.product', + 'connection.profile', + 'connection.buyer'] SOCIAL_AUTH_ITEMBASE_SANDBOX_SCOPE = SOCIAL_AUTH_ITEMBASE_SCOPE .. _Itembase API: http://developers.itembase.com/authentication/index From ffc5bf19d3b199f7605ce450813c51fe70d50fe9 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Tue, 16 Feb 2016 17:44:48 +0100 Subject: [PATCH 770/890] Add test --- social/tests/backends/test_itembase.py | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 social/tests/backends/test_itembase.py diff --git a/social/tests/backends/test_itembase.py b/social/tests/backends/test_itembase.py new file mode 100644 index 000000000..5758e50f7 --- /dev/null +++ b/social/tests/backends/test_itembase.py @@ -0,0 +1,45 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class ItembaseOAuth2Test(OAuth2Test): + backend_path = 'social.backends.itembase.ItembaseOAuth2' + user_data_url = 'https://users.itembase.com/v1/me' + expected_username = 'foobar' + access_token_body = json.dumps({ + "access_token": "foobar-token", + "expires_in": 2592000, + "token_type": "bearer", + "scope": "user.minimal", + "refresh_token": "foobar-refresh-token" + }) + user_data_body = json.dumps({ + "uuid": "a4b91ee7-ec1a-49b9-afce-371dc8797749", + "username": "foobar", + "email": "foobar@itembase.biz", + "first_name": "Foo", + "middle_name": None, + "last_name": "Bar", + "name_format": "first middle last", + "locale": "en", + "preferred_currency": "EUR" + }) + refresh_token_body = json.dumps({ + "access_token": "foobar-new-token", + "expires_in": 2592000, + "token_type": "bearer", + "scope": "user.minimal", + "refresh_token": "foobar-new-refresh-token" + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() + + +class ItembaseOAuth2SandboxTest(OAuth2Test): + backend_path = 'social.backends.itembase.ItembaseOAuth2Sandbox' + user_data_url = 'http://sandbox.users.itembase.io/v1/me' From 940095fd734a01177c51714733610a6452f8c171 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Fri, 19 Feb 2016 10:29:42 +0100 Subject: [PATCH 771/890] Add to readme --- README.rst | 2 ++ docs/backends/index.rst | 1 + docs/backends/itembase.rst | 17 +++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/README.rst b/README.rst index 03df9cd74..0ce0c60ac 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,7 @@ or current ones extended): * Github_ OAuth2 * Google_ OAuth1, OAuth2 and OpenId * Instagram_ OAuth2 + * Itembase_ OAuth2 * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication * Kakao_ OAuth2 https://developer.kakao.com * `Khan Academy`_ OAuth1 @@ -256,6 +257,7 @@ check `django-social-auth LICENSE`_ for details: .. _Github: https://github.com .. _Google: http://google.com .. _Instagram: https://instagram.com +.. _Itembase: https://www.itembase.com .. _LaunchPad: https://help.launchpad.net/YourAccount/OpenID .. _Linkedin: https://www.linkedin.com .. _Live: https://live.com diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 3d6b7e613..e47812b23 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -80,6 +80,7 @@ Social backends github_enterprise google instagram + itembase jawbone justgiving kakao diff --git a/docs/backends/itembase.rst b/docs/backends/itembase.rst index 075a335fc..715d1b427 100644 --- a/docs/backends/itembase.rst +++ b/docs/backends/itembase.rst @@ -30,5 +30,22 @@ Itembase uses OAuth2 for authentication. 'connection.profile', 'connection.buyer'] SOCIAL_AUTH_ITEMBASE_SANDBOX_SCOPE = SOCIAL_AUTH_ITEMBASE_SCOPE + +To use data from the extra scopes, you need to do an extra activation step +that is not in the usual OAuth flow. For that you can extend your pipeline and +add a function that sends the user to an activation URL that Itembase provides. +The method to retrieve the activation data is included in the backend:: + + @partial + def activation(strategy, backend, response, *args, **kwargs): + if backend.name.startswith("itembase"): + + if strategy.session_pop('itembase_activation_in_progress'): + strategy.session_set('itembase_activated', True) + + if not strategy.session_get('itembase_activated'): + activation_data = backend.activation_data(response) + strategy.session_set('itembase_activation_in_progress', True) + return HttpResponseRedirect(activation_data['activation_url']) .. _Itembase API: http://developers.itembase.com/authentication/index From 895f8a0e1f2643cb5b15de6ec1b5d3824ffd6600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 26 Mar 2016 04:06:39 -0300 Subject: [PATCH 772/890] PEP8 --- social/backends/itembase.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/social/backends/itembase.py b/social/backends/itembase.py index 238db7320..8419f38b4 100644 --- a/social/backends/itembase.py +++ b/social/backends/itembase.py @@ -34,11 +34,13 @@ def add_expires(self, data): return data def extra_data(self, user, uid, response, details=None, *args, **kwargs): - data = BaseOAuth2.extra_data(self, user, uid, response, details=details, *args, **kwargs) + data = BaseOAuth2.extra_data(self, user, uid, response, details=details, + *args, **kwargs) return self.add_expires(data) def process_refresh_token_response(self, response, *args, **kwargs): - data = BaseOAuth2.process_refresh_token_response(self, response, *args, **kwargs) + data = BaseOAuth2.process_refresh_token_response(self, response, + *args, **kwargs) return self.add_expires(data) def get_user_details(self, response): @@ -46,14 +48,16 @@ def get_user_details(self, response): return response def user_data(self, access_token, *args, **kwargs): - return self.get_json(self.USER_DETAILS_URL, - headers={'Authorization': 'Bearer {0}'.format(access_token)}) + return self.get_json(self.USER_DETAILS_URL, headers={ + 'Authorization': 'Bearer {0}'.format(access_token) + }) def activation_data(self, response): # returns activation_data dict with activation_url inside # see http://developers.itembase.com/authentication/activation - return self.get_json(self.ACTIVATION_ENDPOINT, - headers={'Authorization': 'Bearer {0}'.format(response['access_token'])}) + return self.get_json(self.ACTIVATION_ENDPOINT, headers={ + 'Authorization': 'Bearer {0}'.format(response['access_token']) + }) @handle_http_errors def auth_complete(self, *args, **kwargs): From 9c8f7094a3439793d1cfbf0a5d65d193890eba8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 27 Mar 2016 02:43:19 -0300 Subject: [PATCH 773/890] PEP8 and minor code simplification --- docs/backends/arcgis.rst | 13 ++++++------- social/backends/arcgis.py | 4 +--- social/tests/backends/test_arcgis.py | 16 +++++++++------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/backends/arcgis.rst b/docs/backends/arcgis.rst index 6c402cd45..1ab43a271 100644 --- a/docs/backends/arcgis.rst +++ b/docs/backends/arcgis.rst @@ -11,16 +11,15 @@ OAuth2 1. Add the OAuth2 backend to your settings page:: - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.arcgis.ArcGISOAuth2', - ... + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.arcgis.ArcGISOAuth2', + ... ) 2. Fill ``Client Id`` and ``Client Secret`` values in the settings:: - SOCIAL_AUTH_ARCGIS_KEY = '' - SOCIAL_AUTH_ARCGIS_SECRET = '' - + SOCIAL_AUTH_ARCGIS_KEY = '' + SOCIAL_AUTH_ARCGIS_SECRET = '' .. _ArcGIS Developer Center: https://developers.arcgis.com/ diff --git a/social/backends/arcgis.py b/social/backends/arcgis.py index 7f54b3c48..1a2b59ac7 100644 --- a/social/backends/arcgis.py +++ b/social/backends/arcgis.py @@ -24,12 +24,10 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - data = self.get_json( + return self.get_json( 'https://www.arcgis.com/sharing/rest/community/self', params={ 'token': access_token, 'f': 'json' } ) - - return data diff --git a/social/tests/backends/test_arcgis.py b/social/tests/backends/test_arcgis.py index 39d4b6e84..90ca9c308 100644 --- a/social/tests/backends/test_arcgis.py +++ b/social/tests/backends/test_arcgis.py @@ -8,16 +8,18 @@ class ArcGISOAuth2Test(OAuth2Test): expected_username = 'gis@rocks.com' user_data_body = json.dumps({ - "first_name": "Gis", - "last_name": "Rocks", - "email": "gis@rocks.com", - "fullName": "Gis Rocks", - "username": "gis@rocks.com" + 'first_name': 'Gis', + 'last_name': 'Rocks', + 'email': 'gis@rocks.com', + 'fullName': 'Gis Rocks', + 'username': 'gis@rocks.com' }) access_token_body = json.dumps({ - "access_token": "CM-gcB85taGhRmoI7l3PSGaXUNsaLkTg-dHH7XtA9DnlinPYKBBrIvFzhd1JtDhh7hEwSv_6eLLcLtUqe3gD6i1yaYYFpUQJwy8KEujke5AE87tP9XIoMtp4_l320pUL", - "expires_in": 86400 + 'access_token': 'CM-gcB85taGhRmoI7l3PSGaXUNsaLkTg-dHH7XtA9Dnlin' \ + 'PYKBBrIvFzhd1JtDhh7hEwSv_6eLLcLtUqe3gD6i1yaYYF' \ + 'pUQJwy8KEujke5AE87tP9XIoMtp4_l320pUL', + 'expires_in': 86400 }) def test_login(self): From 9440655649cd2c0db558f9d308d17c4d7347ab58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 27 Mar 2016 02:52:10 -0300 Subject: [PATCH 774/890] PEP8 and code style --- docs/backends/drip.rst | 5 +++-- docs/backends/index.rst | 1 + social/backends/drip.py | 6 +++--- social/tests/backends/test_drip.py | 18 +++++++++++------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/backends/drip.rst b/docs/backends/drip.rst index b09c64e2b..88f09a2ed 100644 --- a/docs/backends/drip.rst +++ b/docs/backends/drip.rst @@ -1,11 +1,12 @@ Drip -========= +==== Drip uses OAuth v2 for Authentication. - Register a new application with `Drip`_, and -- fill ``Client ID`` and ``Client Secret`` from getdrip.com values in the settings:: +- fill ``Client ID`` and ``Client Secret`` from getdrip.com values in + the settings:: SOCIAL_AUTH_DRIP_KEY = '' SOCIAL_AUTH_DRIP_SECRET = '' diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 8a8009952..cf37adc81 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -70,6 +70,7 @@ Social backends docker douban dribbble + drip dropbox eveonline evernote diff --git a/social/backends/drip.py b/social/backends/drip.py index 96ae93480..4bfeb95a1 100644 --- a/social/backends/drip.py +++ b/social/backends/drip.py @@ -20,6 +20,6 @@ def get_user_details(self, response): 'username': response['users'][0]['email']} def user_data(self, access_token, *args, **kwargs): - return self.get_json( - 'https://api.getdrip.com/v2/user', - headers={'Authorization': 'Bearer %s' % access_token}) + return self.get_json('https://api.getdrip.com/v2/user', headers={ + 'Authorization': 'Bearer %s' % access_token + }) diff --git a/social/tests/backends/test_drip.py b/social/tests/backends/test_drip.py index e9424acda..20cb620f0 100644 --- a/social/tests/backends/test_drip.py +++ b/social/tests/backends/test_drip.py @@ -8,15 +8,19 @@ class DripOAuthTest(OAuth2Test): user_data_url = 'https://api.getdrip.com/v2/user' expected_username = 'other@example.com' access_token_body = json.dumps({ - "access_token": "822bbf7cd12243df", - "token_type": "bearer", - "scope": "public" + 'access_token': '822bbf7cd12243df', + 'token_type': 'bearer', + 'scope': 'public' }) - user_data_body = json.dumps( - {'users': [ - {'email': 'other@example.com', 'name': None} - ]}) + user_data_body = json.dumps({ + 'users': [ + { + 'email': 'other@example.com', + 'name': None + } + ] + }) def test_login(self): self.do_login() From 97c05e580aeea5460ed72fa2a0191ff9d0da4331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 27 Mar 2016 03:06:37 -0300 Subject: [PATCH 775/890] Remove unneeded flag and method --- social/backends/pinterest.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/social/backends/pinterest.py b/social/backends/pinterest.py index 3a907034c..f14563374 100644 --- a/social/backends/pinterest.py +++ b/social/backends/pinterest.py @@ -19,11 +19,6 @@ class PinterestOAuth2(BaseOAuth2): REDIRECT_STATE = False ACCESS_TOKEN_METHOD = 'POST' SSL_PROTOCOL = ssl.PROTOCOL_TLSv1 - FAKE_HTTPS = False - - def get_redirect_uri(self, state=None): - url = super(PinterestOAuth2, self).get_redirect_uri(state) - return self.FAKE_HTTPS and url.replace('http:', 'https:') or url def user_data(self, access_token, *args, **kwargs): response = self.get_json('https://api.pinterest.com/v1/me/', From 15015ae385af249c7e7bdcd979e89357b62f1d00 Mon Sep 17 00:00:00 2001 From: EJ Date: Mon, 28 Mar 2016 06:30:40 +0900 Subject: [PATCH 776/890] modifed wrong key names in pocket.py There is a wrong name of the social auth key. I think, 'POCKET_CONSUMER_KEY' shoud be a 'SOCIAL_AUTH_POCKET_KEY'. 'POCKET_CONSUMER_KEY' makes a ''400 Client Error". --- social/backends/pocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/pocket.py b/social/backends/pocket.py index bb48b71f2..b86960e23 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -26,7 +26,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): def auth_url(self): data = { - 'consumer_key': self.setting('POCKET_CONSUMER_KEY'), + 'consumer_key': self.setting('SOCIAL_AUTH_POCKET_KEY'), 'redirect_uri': self.redirect_uri, } token = self.get_json(self.REQUEST_TOKEN_URL, data=data)['code'] @@ -37,7 +37,7 @@ def auth_url(self): @handle_http_errors def auth_complete(self, *args, **kwargs): data = { - 'consumer_key': self.setting('POCKET_CONSUMER_KEY'), + 'consumer_key': self.setting('SOCIAL_AUTH_POCKET_KEY'), 'code': self.strategy.session_get('pocket_request_token'), } response = self.get_json(self.ACCESS_TOKEN_URL, data=data) From 2185a15d9cc651561eaa324c6daa30a7f4c46f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 28 Mar 2016 02:47:42 -0300 Subject: [PATCH 777/890] Amend pocket backend key name --- social/backends/pocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/pocket.py b/social/backends/pocket.py index b86960e23..3ea640c74 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -26,7 +26,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): def auth_url(self): data = { - 'consumer_key': self.setting('SOCIAL_AUTH_POCKET_KEY'), + 'consumer_key': self.setting('POCKET_KEY'), 'redirect_uri': self.redirect_uri, } token = self.get_json(self.REQUEST_TOKEN_URL, data=data)['code'] @@ -37,7 +37,7 @@ def auth_url(self): @handle_http_errors def auth_complete(self, *args, **kwargs): data = { - 'consumer_key': self.setting('SOCIAL_AUTH_POCKET_KEY'), + 'consumer_key': self.setting('POCKET_KEY'), 'code': self.strategy.session_get('pocket_request_token'), } response = self.get_json(self.ACCESS_TOKEN_URL, data=data) From ba0763a5b0d2b86cd12732c65337ed7e4101c755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 28 Mar 2016 03:02:04 -0300 Subject: [PATCH 778/890] Fix pocket key name --- social/backends/pocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/pocket.py b/social/backends/pocket.py index 3ea640c74..49b73d55a 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -26,7 +26,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): def auth_url(self): data = { - 'consumer_key': self.setting('POCKET_KEY'), + 'consumer_key': self.setting('KEY'), 'redirect_uri': self.redirect_uri, } token = self.get_json(self.REQUEST_TOKEN_URL, data=data)['code'] @@ -37,7 +37,7 @@ def auth_url(self): @handle_http_errors def auth_complete(self, *args, **kwargs): data = { - 'consumer_key': self.setting('POCKET_KEY'), + 'consumer_key': self.setting('KEY'), 'code': self.strategy.session_get('pocket_request_token'), } response = self.get_json(self.ACCESS_TOKEN_URL, data=data) From b2f593ad36a13d87deee09977d7f07776a70b800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 28 Mar 2016 03:30:35 -0300 Subject: [PATCH 779/890] Apply code-style --- social/backends/ngpvan.py | 15 +++-- social/tests/backends/test_ngpvan.py | 87 ++++++++++++++-------------- social/tests/models.py | 2 +- 3 files changed, 56 insertions(+), 48 deletions(-) diff --git a/social/backends/ngpvan.py b/social/backends/ngpvan.py index 42a34c81e..0700eeb93 100644 --- a/social/backends/ngpvan.py +++ b/social/backends/ngpvan.py @@ -3,9 +3,10 @@ http://developers.ngpvan.com/action-id """ -from social.backends.open_id import OpenIdAuth from openid.extensions import ax +from social.backends.open_id import OpenIdAuth + class ActionIDOpenID(OpenIdAuth): """ @@ -42,20 +43,24 @@ def setup_request(self, params=None): fetch_request.add(ax.AttrInfo( 'http://openid.net/schema/contact/internet/email', alias='ngpvanemail', - required=True)) + required=True + )) fetch_request.add(ax.AttrInfo( 'http://openid.net/schema/contact/phone/business', alias='ngpvanphone', - required=False)) + required=False + )) fetch_request.add(ax.AttrInfo( 'http://openid.net/schema/namePerson/first', alias='ngpvanfirstname', - required=False)) + required=False + )) fetch_request.add(ax.AttrInfo( 'http://openid.net/schema/namePerson/last', alias='ngpvanlastname', - required=False)) + required=False + )) request.addExtension(fetch_request) return request diff --git a/social/tests/backends/test_ngpvan.py b/social/tests/backends/test_ngpvan.py index 1189839a1..862022b41 100644 --- a/social/tests/backends/test_ngpvan.py +++ b/social/tests/backends/test_ngpvan.py @@ -14,29 +14,28 @@ class NGPVANActionIDOpenIDTest(OpenIdTest): """Test the NGP VAN ActionID OpenID 1.1 Backend""" backend_path = 'social.backends.ngpvan.ActionIDOpenID' expected_username = 'testuser@user.local' - discovery_body = ' '.join( - [ - '', - '', - '', - '', - 'http://specs.openid.net/auth/2.0/signon', - 'http://openid.net/extensions/sreg/1.1', - 'http://axschema.org/contact/email', - 'https://accounts.ngpvan.com/OpenId/Provider', - '', - '', - 'http://openid.net/signon/1.0', - 'http://openid.net/extensions/sreg/1.1', - 'http://axschema.org/contact/email', - 'https://accounts.ngpvan.com/OpenId/Provider', - '', - '', - '', - ]) + discovery_body = ' '.join([ + '', + '', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/extensions/sreg/1.1', + 'http://axschema.org/contact/email', + 'https://accounts.ngpvan.com/OpenId/Provider', + '', + '', + 'http://openid.net/signon/1.0', + 'http://openid.net/extensions/sreg/1.1', + 'http://axschema.org/contact/email', + 'https://accounts.ngpvan.com/OpenId/Provider', + '', + '', + '' + ]) server_response = urlencode({ 'openid.claimed_id': 'https://accounts.ngpvan.com/user/abcd123', 'openid.identity': 'https://accounts.ngpvan.com/user/abcd123', @@ -91,17 +90,20 @@ def setUp(self): HTTPretty.POST, 'https://accounts.ngpvan.com/Home/Xrds', status=200, - body=self.discovery_body) + body=self.discovery_body + ) HTTPretty.register_uri( HTTPretty.GET, 'https://accounts.ngpvan.com/user/abcd123', status=200, - body=self.discovery_body) + body=self.discovery_body + ) HTTPretty.register_uri( HTTPretty.GET, 'https://accounts.ngpvan.com/OpenId/Provider', status=200, - body=self.discovery_body) + body=self.discovery_body + ) def test_login(self): """Test the login flow using python-social-auth's built in test""" @@ -115,16 +117,13 @@ def test_get_ax_attributes(self): """Test that the AX attributes that NGP VAN responds with are present""" records = self.backend.get_ax_attributes() - self.assertEqual( - records, - [ - ('http://openid.net/schema/contact/internet/email', 'email'), - ('http://openid.net/schema/contact/phone/business', 'phone'), - ('http://openid.net/schema/namePerson/first', 'first_name'), - ('http://openid.net/schema/namePerson/last', 'last_name'), - ('http://openid.net/schema/namePerson', 'fullname'), - ] - ) + self.assertEqual(records, [ + ('http://openid.net/schema/contact/internet/email', 'email'), + ('http://openid.net/schema/contact/phone/business', 'phone'), + ('http://openid.net/schema/namePerson/first', 'first_name'), + ('http://openid.net/schema/namePerson/last', 'last_name'), + ('http://openid.net/schema/namePerson', 'fullname'), + ]) def test_setup_request(self): """Test the setup_request functionality in the NGP VAN backend""" @@ -143,16 +142,20 @@ def test_setup_request(self): # Verify the individual attribute properties self.assertEqual( inputs['openid.ax.type.ngpvanemail'], - 'http://openid.net/schema/contact/internet/email') + 'http://openid.net/schema/contact/internet/email' + ) self.assertEqual( inputs['openid.ax.type.ngpvanfirstname'], - 'http://openid.net/schema/namePerson/first') + 'http://openid.net/schema/namePerson/first' + ) self.assertEqual( inputs['openid.ax.type.ngpvanlastname'], - 'http://openid.net/schema/namePerson/last') + 'http://openid.net/schema/namePerson/last' + ) self.assertEqual( inputs['openid.ax.type.ngpvanphone'], - 'http://openid.net/schema/contact/phone/business') + 'http://openid.net/schema/contact/phone/business' + ) def test_user_data(self): """Ensure that the correct user data is being passed to create_user""" @@ -167,7 +170,6 @@ def test_user_data(self): ] }) user = self.do_start() - self.assertEqual(user.username, u'testuser@user.local') self.assertEqual(user.email, u'testuser@user.local') self.assertEqual(user.extra_user_fields['phone'], u'+12015555555') @@ -190,4 +192,5 @@ def test_association_uid(self): user = self.do_start() self.assertEqual( user.social_user.uid, - 'https://accounts.ngpvan.com/user/abcd123') + 'https://accounts.ngpvan.com/user/abcd123' + ) diff --git a/social/tests/models.py b/social/tests/models.py index d142d9260..7d9d6acbd 100644 --- a/social/tests/models.py +++ b/social/tests/models.py @@ -1,7 +1,7 @@ import base64 from social.storage.base import UserMixin, NonceMixin, AssociationMixin, \ - CodeMixin, BaseStorage + CodeMixin, BaseStorage class BaseModel(object): From 2c1494ce32244240fab3895890ab846788741cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 28 Mar 2016 03:37:42 -0300 Subject: [PATCH 780/890] Style doc and code --- docs/backends/fitbit.rst | 14 +++++++++----- social/backends/fitbit.py | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/backends/fitbit.rst b/docs/backends/fitbit.rst index 8bd3389d6..057a84c85 100644 --- a/docs/backends/fitbit.rst +++ b/docs/backends/fitbit.rst @@ -1,17 +1,20 @@ Fitbit ====== -Fitbit supports both OAuth 2.0 and OAuth 1.0a logins. -OAuth 2 is preferred for new integrations, as OAuth 1.0a does not support getting heartrate or location and will be deprecated in the future. +Fitbit supports both OAuth 2.0 and OAuth 1.0a logins. OAuth 2 is +preferred for new integrations, as OAuth 1.0a does not support getting +heartrate or location and will be deprecated in the future. 1. Register a new OAuth Consumer `here`_ -2. Configure the appropriate settings for OAuth 2.0 or OAuth 1.0a (see below). +2. Configure the appropriate settings for OAuth 2.0 or OAuth 1.0a (see + below). OAuth 2.0 or OAuth 1.0a ----------------------- -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: +- Fill ``Consumer Key`` and ``Consumer Secret`` values in the + settings:: SOCIAL_AUTH_FITBIT_KEY = '' SOCIAL_AUTH_FITBIT_SECRET = '' @@ -19,7 +22,8 @@ OAuth 2.0 or OAuth 1.0a OAuth 2.0 specific settings --------------------------- -By default, only the ``profile`` scope is requested. To request more scopes, set SOCIAL_AUTH_FITBIT_SCOPE:: +By default, only the ``profile`` scope is requested. To request more +scopes, set SOCIAL_AUTH_FITBIT_SCOPE:: SOCIAL_AUTH_FITBIT_SCOPE = [ 'activity', diff --git a/social/backends/fitbit.py b/social/backends/fitbit.py index 16c4f77d2..35d9bf1de 100644 --- a/social/backends/fitbit.py +++ b/social/backends/fitbit.py @@ -56,9 +56,10 @@ def user_data(self, access_token, *args, **kwargs): 'https://api.fitbit.com/1/user/-/profile.json', headers=auth_header )['user'] + def auth_headers(self): return { 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) )) - } \ No newline at end of file + } From 19265539a1da1ff71e6951e8f3d74f3ea9a1f494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 28 Mar 2016 03:56:33 -0300 Subject: [PATCH 781/890] PEP8 --- .../default/migrations/0001_initial.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 133e5dd62..9917f67c3 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -8,15 +8,21 @@ from social.utils import setting_name USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ - getattr(settings, 'AUTH_USER_MODEL', None) or 'auth.User' + getattr(settings, 'AUTH_USER_MODEL', None) or \ + 'auth.User' UID_LENGTH = getattr(settings, setting_name('UID_LENGTH'), 255) -NONCE_SERVER_URL_LENGTH = getattr(settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255) -ASSOCIATION_SERVER_URL_LENGTH = getattr(settings, setting_name('ASSOCIATION_SERVER_URL_LENGTH'), 255) -ASSOCIATION_HANDLE_LENGTH = getattr(settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255) +NONCE_SERVER_URL_LENGTH = getattr( + settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255 +) +ASSOCIATION_SERVER_URL_LENGTH = getattr( + settings, setting_name('ASSOCIATION_SERVER_URL_LENGTH'), 255 +) +ASSOCIATION_HANDLE_LENGTH = getattr( + settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255 +) class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(USER_MODEL), ] @@ -28,8 +34,10 @@ class Migration(migrations.Migration): ('id', models.AutoField( verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('server_url', models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH)), - ('handle', models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH)), + ('server_url', + models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH)), + ('handle', + models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH)), ('secret', models.CharField(max_length=255)), ('issued', models.IntegerField()), ('lifetime', models.IntegerField()), @@ -62,8 +70,10 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField( verbose_name='ID', serialize=False, auto_created=True, - primary_key=True)), - ('server_url', models.CharField(max_length=NONCE_SERVER_URL_LENGTH)), + primary_key=True + )), + ('server_url', + models.CharField(max_length=NONCE_SERVER_URL_LENGTH)), ('timestamp', models.IntegerField()), ('salt', models.CharField(max_length=65)), ], From 769bd902f0a9424c48827721a64efe2768b7a197 Mon Sep 17 00:00:00 2001 From: st4lk Date: Sat, 2 Apr 2016 22:14:37 +0300 Subject: [PATCH 782/890] add passing response to AuthCancel exception, if it available --- social/backends/evernote.py | 2 +- social/backends/weixin.py | 2 +- social/exceptions.py | 4 ++++ social/tests/backends/test_evernote.py | 6 ++++-- social/tests/backends/test_facebook.py | 24 +++++++++++++++++++++++- social/utils.py | 2 +- 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/social/backends/evernote.py b/social/backends/evernote.py index 2fde3e5cb..6a66585e0 100644 --- a/social/backends/evernote.py +++ b/social/backends/evernote.py @@ -50,7 +50,7 @@ def access_token(self, token): except HTTPError as err: # Evernote returns a 401 error when AuthCanceled if err.response.status_code == 401: - raise AuthCanceled(self) + raise AuthCanceled(self, response=err.response) else: raise diff --git a/social/backends/weixin.py b/social/backends/weixin.py index b1308f313..1b5c2d500 100644 --- a/social/backends/weixin.py +++ b/social/backends/weixin.py @@ -89,7 +89,7 @@ def auth_complete(self, *args, **kwargs): ) except HTTPError as err: if err.response.status_code == 400: - raise AuthCanceled(self) + raise AuthCanceled(self, response=err.response) else: raise except KeyError: diff --git a/social/exceptions.py b/social/exceptions.py index 88e11551c..aa174c970 100644 --- a/social/exceptions.py +++ b/social/exceptions.py @@ -41,6 +41,10 @@ def __str__(self): class AuthCanceled(AuthException): """Auth process was canceled by user.""" + def __init__(self, *args, **kwargs): + self.response = kwargs.pop('response', None) + super(AuthCanceled, self).__init__(*args, **kwargs) + def __str__(self): return 'Authentication process canceled' diff --git a/social/tests/backends/test_evernote.py b/social/tests/backends/test_evernote.py index f83907333..1cf1b35ca 100644 --- a/social/tests/backends/test_evernote.py +++ b/social/tests/backends/test_evernote.py @@ -34,12 +34,14 @@ class EvernoteOAuth1CanceledTest(EvernoteOAuth1Test): access_token_status = 401 def test_login(self): - with self.assertRaises(AuthCanceled): + with self.assertRaises(AuthCanceled) as cm: self.do_login() + self.assertTrue(cm.exception.response is not None) def test_partial_pipeline(self): - with self.assertRaises(AuthCanceled): + with self.assertRaises(AuthCanceled) as cm: self.do_partial_pipeline() + self.assertTrue(cm.exception.response is not None) class EvernoteOAuth1ErrorTest(EvernoteOAuth1Test): diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py index fc1aa5feb..166d75327 100644 --- a/social/tests/backends/test_facebook.py +++ b/social/tests/backends/test_facebook.py @@ -1,6 +1,6 @@ import json -from social.exceptions import AuthUnknownError +from social.exceptions import AuthUnknownError, AuthCanceled from social.tests.backends.oauth import OAuth2Test @@ -42,3 +42,25 @@ def test_login(self): def test_partial_pipeline(self): with self.assertRaises(AuthUnknownError): self.do_partial_pipeline() + + +class FacebookOAuth2AuthCancelTest(FacebookOAuth2Test): + access_token_status = 400 + access_token_body = json.dumps({ + 'error': { + 'message': "redirect_uri isn't an absolute URI. Check RFC 3986.", + 'code': 191, + 'type': 'OAuthException', + 'fbtrace_id': '123Abc' + } + }) + + def test_login(self): + with self.assertRaises(AuthCanceled) as cm: + self.do_login() + self.assertIn('error', cm.exception.response.json()) + + def test_partial_pipeline(self): + with self.assertRaises(AuthCanceled) as cm: + self.do_partial_pipeline() + self.assertIn('error', cm.exception.response.json()) diff --git a/social/utils.py b/social/utils.py index 0a473af15..0b5a50759 100644 --- a/social/utils.py +++ b/social/utils.py @@ -229,7 +229,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) except requests.HTTPError as err: if err.response.status_code == 400: - raise AuthCanceled(args[0]) + raise AuthCanceled(args[0], response=err.response) elif err.response.status_code == 503: raise AuthUnreachableProvider(args[0]) else: From 7da22bfe2106b6041b4ac72dda177ff732291fc2 Mon Sep 17 00:00:00 2001 From: Sean Hayes Date: Thu, 31 Mar 2016 15:41:54 -0700 Subject: [PATCH 783/890] Fixes bug where partial pipelines from abandoned login attempts will be resumed on new login attempts. This can happen with the email backend, which sends data directly to auth:complete, thereby bypassing the call to `clean_partial_pipeline` in `do_auth`. --- social/tests/test_utils.py | 28 ++++++++++++++++++++++++++++ social/utils.py | 17 +++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index 7bd8db089..28d595ed7 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -121,6 +121,30 @@ def test_absolute_uri(self): class PartialPipelineData(unittest.TestCase): + def test_returns_partial_when_uid_and_email_do_match(self): + email = 'foo@example.com' + backend = self._backend({'uid': email}) + backend.strategy.request_data.return_value = { + backend.ID_KEY: email + } + key, val = ('foo', 'bar') + _, xkwargs = partial_pipeline_data(backend, None, + *(), **dict([(key, val)])) + self.assertTrue(key in xkwargs) + self.assertEqual(xkwargs[key], val) + self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0) + + def test_clean_pipeline_when_uid_does_not_match(self): + backend = self._backend({'uid': 'foo@example.com'}) + backend.strategy.request_data.return_value = { + backend.ID_KEY: 'bar@example.com' + } + key, val = ('foo', 'bar') + ret = partial_pipeline_data(backend, None, + *(), **dict([(key, val)])) + self.assertIsNone(ret) + self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 1) + def test_kwargs_included_in_result(self): backend = self._backend() key, val = ('foo', 'bar') @@ -128,6 +152,7 @@ def test_kwargs_included_in_result(self): *(), **dict([(key, val)])) self.assertTrue(key in xkwargs) self.assertEqual(xkwargs[key], val) + self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0) def test_update_user(self): user = object() @@ -135,15 +160,18 @@ def test_update_user(self): _, xkwargs = partial_pipeline_data(backend, user) self.assertTrue('user' in xkwargs) self.assertEqual(xkwargs['user'], user) + self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0) def _backend(self, session_kwargs=None): strategy = Mock() strategy.request = None + strategy.request_data.return_value = {} strategy.session_get.return_value = object() strategy.partial_from_session.return_value = \ (0, 'mock-backend', [], session_kwargs or {}) backend = Mock() + backend.ID_KEY = 'email' backend.name = 'mock-backend' backend.strategy = strategy return backend diff --git a/social/utils.py b/social/utils.py index 0a473af15..37c04a864 100644 --- a/social/utils.py +++ b/social/utils.py @@ -166,7 +166,24 @@ def partial_pipeline_data(backend, user=None, *args, **kwargs): if partial: idx, backend_name, xargs, xkwargs = \ backend.strategy.partial_from_session(partial) + + partial_matches_request = False + if backend_name == backend.name: + partial_matches_request = True + + req_data = backend.strategy.request_data() + # Normally when resuming a pipeline, request_data will be empty. We + # only need to check for a uid match if new data was provided (i.e. + # if current request specifies the ID_KEY). + if backend.ID_KEY in req_data: + id_from_partial = xkwargs.get('uid') + id_from_request = req_data.get(backend.ID_KEY) + + if id_from_partial != id_from_request: + partial_matches_request = False + + if partial_matches_request: kwargs.setdefault('pipeline_index', idx) if user: # don't update user if it's None kwargs.setdefault('user', user) From ffcb1e96d1c67a50eec76604ac4fe3ad0146fa04 Mon Sep 17 00:00:00 2001 From: Robin Stephenson Date: Wed, 6 Apr 2016 18:42:33 +0100 Subject: [PATCH 784/890] Removed broken link --- docs/use_cases.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/use_cases.rst b/docs/use_cases.rst index 3ac58560f..4f7c92641 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -143,9 +143,6 @@ will be done by AJAX. It doesn't return the user information, but that's something that can be extended and filled to suit the project where it's going to be used. -This topic is well addressed in `A Rest API using Django and authentication -with OAuth2 AND third parties!`_ wrote by `Félix Descôteaux`_. - Multiple scopes per provider ---------------------------- @@ -313,5 +310,3 @@ Set this pipeline after ``social_user``:: .. _python-social-auth: https://github.com/omab/python-social-auth .. _People API endpoint: https://developers.google.com/+/api/latest/people/list -.. _Félix Descôteaux: https://twitter.com/FelixDescoteaux -.. _A Rest API using Django and authentication with OAuth2 AND third parties!: http://httplambda.com/a-rest-api-with-django-and-oauthw-authentication/ From e95ce614613f50fbbce30817fc3cc032acd359f9 Mon Sep 17 00:00:00 2001 From: Alex Dancho Date: Thu, 7 Apr 2016 11:14:33 -0400 Subject: [PATCH 785/890] Add a backend for Classlink. --- .gitignore | 3 +++ social/backends/classlink.py | 44 ++++++++++++++++++++++++++++++++++++ social/storage/base.py | 3 ++- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 social/backends/classlink.py diff --git a/.gitignore b/.gitignore index f6bf7759d..7c2001d47 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ nosetests.xml .project .pydevproject +# PyCharm +.idea/ + test.db local_settings.py sessions/ diff --git a/social/backends/classlink.py b/social/backends/classlink.py new file mode 100644 index 000000000..31f8f74d2 --- /dev/null +++ b/social/backends/classlink.py @@ -0,0 +1,44 @@ +from social.backends.oauth import BaseOAuth2 + + +class ClasslinkOAuth(BaseOAuth2): + """ + Classlink OAuth authentication backend. + + Docs: https://developer.classlink.com/docs/oauth2-workflow + """ + name = 'classlink' + AUTHORIZATION_URL = 'https://launchpad.classlink.com/oauth2/v2/auth' + ACCESS_TOKEN_URL = 'https://launchpad.classlink.com/oauth2/v2/token' + ACCESS_TOKEN_METHOD = 'POST' + DEFAULT_SCOPE = ['profile'] + REDIRECT_STATE = False + SCOPE_SEPARATOR = ' ' + + def get_user_id(self, details, response): + """Return user unique id provided by service""" + return response['UserId'] + + def get_user_details(self, response): + """Return user details from Classlink account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('FirstName'), + last_name=response.get('LastName') + ) + + return { + 'username': response.get('Email') or response.get('LoginId'), + 'email': response.get('Email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, + } + + def user_data(self, token, *args, **kwargs): + """Loads user data from service""" + url = 'https://nodeapi.classlink.com/v2/my/info' + auth_header = {"Authorization": "Bearer %s" % token} + try: + return self.get_json(url, headers=auth_header) + except ValueError: + return None diff --git a/social/storage/base.py b/social/storage/base.py index f5f57af2c..5df42e372 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -81,7 +81,8 @@ def expiration_datetime(self): def set_extra_data(self, extra_data=None): if extra_data and self.extra_data != extra_data: - if self.extra_data and not isinstance(self.extra_data, str): + if self.extra_data and not isinstance( + self.extra_data, six.string_types): self.extra_data.update(extra_data) else: self.extra_data = extra_data From 7e2d46d67576507afc729e069718b2b2ce716996 Mon Sep 17 00:00:00 2001 From: Joway Date: Mon, 11 Apr 2016 08:19:02 +0800 Subject: [PATCH 786/890] add coding oauth add coding(https://coding.net) oauth2 --- social/backends/coding.py | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 social/backends/coding.py diff --git a/social/backends/coding.py b/social/backends/coding.py new file mode 100644 index 000000000..ee9b2afa3 --- /dev/null +++ b/social/backends/coding.py @@ -0,0 +1,45 @@ +""" +Coding OAuth2 backend, docs at: +""" +from six.moves.urllib.parse import urljoin + +from social.backends.oauth import BaseOAuth2 + + +class CodingOAuth2(BaseOAuth2): + """Coding OAuth authentication backend""" + + name = 'coding' + API_URL = 'https://coding.net/api/' + AUTHORIZATION_URL = 'https://coding.net/oauth_authorize.html' + ACCESS_TOKEN_URL = 'https://coding.net/api/oauth/access_token' + ACCESS_TOKEN_METHOD = 'POST' + SCOPE_SEPARATOR = ',' + DEFAULT_SCOPE = ['user'] + REDIRECT_STATE = False + + def api_url(self): + return self.API_URL + + def get_user_details(self, response): + """Return user details from Github account""" + fullname, first_name, last_name = self.get_user_names( + response.get('name') + ) + return {'username': response.get('name'), + 'email': response.get('email') or '', + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + data = self._user_data(access_token) + if data.get('code') != 0: + # 获取失败 + pass + return data.get('data') + + def _user_data(self, access_token, path=None): + url = urljoin(self.api_url(), 'account/current_user{0}'.format(path or '')) + return self.get_json(url, params={'access_token': access_token}) From 57501b129ad3e5826333fa74889bf911a61c4cca Mon Sep 17 00:00:00 2001 From: Scott Vitale Date: Mon, 11 Apr 2016 20:13:18 -0600 Subject: [PATCH 787/890] Add support for Untappd as an OAuth v2 backend --- README.rst | 2 + docs/backends/index.rst | 1 + docs/backends/untappd.rst | 27 ++++++++++ social/backends/untappd.py | 100 +++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 docs/backends/untappd.rst create mode 100644 social/backends/untappd.py diff --git a/README.rst b/README.rst index ec4e25a60..20c17ce7c 100644 --- a/README.rst +++ b/README.rst @@ -123,6 +123,7 @@ or current ones extended): * Twilio_ Auth * Twitter_ OAuth1 * Uber_ OAuth2 + * Untappd_ OAuth2 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 * Withings_ OAuth1 @@ -325,3 +326,4 @@ check `django-social-auth LICENSE`_ for details: .. _PixelPin: http://pixelpin.co.uk .. _Zotero: http://www.zotero.org/ .. _Pinterest: https://www.pinterest.com +.. _Untappd: https://untappd.com/ diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 97cf5fb85..819c22780 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -140,6 +140,7 @@ Social backends twitch twitter uber + untappd vend vimeo vk diff --git a/docs/backends/untappd.rst b/docs/backends/untappd.rst new file mode 100644 index 000000000..bb1157a91 --- /dev/null +++ b/docs/backends/untappd.rst @@ -0,0 +1,27 @@ +Untappd +======= + +Untappd uses OAuth v2 for Authentication, check the `official docs`_. + +- Create an app by filling out the form here: `Add App`_ + +- Apps are approved on a one-by-one basis, so you'll need to wait a few days to get your client ID and secret. + +- Fill ``Client ID`` and ``Client Secret`` values in the settings:: + + SOCIAL_AUTH_UNTAPPD_KEY = '' + SOCIAL_AUTH_UNTAPPD_SECRET = '' + +- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting:: + + AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.untappd.UntappdOAuth2', + ... + ) + +- Then you can start using ``{% url social:begin 'untappd' %}`` in + your templates + +.. _official docs: https://untappd.com/api/docs +.. _Add App: https://untappd.com/api/register?register=new diff --git a/social/backends/untappd.py b/social/backends/untappd.py new file mode 100644 index 000000000..8d5b71df6 --- /dev/null +++ b/social/backends/untappd.py @@ -0,0 +1,100 @@ +import requests + +from social.backends.oauth import BaseOAuth2 +from social.exceptions import AuthFailed +from social.utils import handle_http_errors + + +class UntappdOAuth2(BaseOAuth2): + """Untappd OAuth2 authentication backend""" + name = 'untappd' + AUTHORIZATION_URL = 'https://untappd.com/oauth/authenticate/' + ACCESS_TOKEN_URL = 'https://untappd.com/oauth/authorize/' + BASE_API_URL = 'https://api.untappd.com' + USER_INFO_URL = BASE_API_URL + '/v4/user/info/' + ACCESS_TOKEN_METHOD = 'GET' + STATE_PARAMETER = False + REDIRECT_STATE = False + EXTRA_DATA = [ + ('id', 'id'), + ('bio', 'bio'), + ('date_joined', 'date_joined'), + ('location', 'location'), + ('url', 'url'), + ('user_avatar', 'user_avatar'), + ('user_avatar_hd', 'user_avatar_hd'), + ('user_cover_photo', 'user_cover_photo') + ] + + def auth_params(self, state=None): + client_id, client_secret = self.get_key_and_secret() + params = { + 'client_id': client_id, + 'redirect_url': self.get_redirect_uri(), + 'response_type': self.RESPONSE_TYPE + } + return params + + def process_error(self, data): + """ All errors from Untappd are contained in the 'meta' key of the response. """ + response_code = data.get('meta', {}).get('http_code') + if response_code is not None and response_code != requests.codes.ok: + raise AuthFailed(self, data['meta']['error_detail']) + + @handle_http_errors + def auth_complete(self, *args, **kwargs): + """Completes login process, must return user instance""" + client_id, client_secret = self.get_key_and_secret() + code = self.data.get('code') + + self.process_error(self.data) + + # Untapped sends the access token request with URL parameters, not a body + response = self.request_access_token( + self.access_token_url(), + method=self.ACCESS_TOKEN_METHOD, + params={ + 'response_type': 'code', + 'code': code, + 'client_id': client_id, + 'client_secret': client_secret, + 'redirect_url': self.get_redirect_uri() + } + ) + + self.process_error(response) + + # Both the access_token and the rest of the response are buried in the 'response' key + return self.do_auth(response['response']['access_token'], response=response['response'], + *args, **kwargs) + + def get_user_details(self, response): + """Return user details from an Untappd account""" + # Start with the user data as it was returned + user_data = response['user'] + + # Make a few updates to match expected key names + user_data.update({ + 'username': user_data.get('user_name'), + 'email': user_data.get('settings', {}).get('email_address', ''), + 'first_name': user_data.get('first_name'), + 'last_name': user_data.get('last_name'), + 'fullname': user_data.get('first_name') + ' ' + user_data.get('last_name') + }) + return user_data + + def get_user_id(self, details, response): + """Return a unique ID for the current user, by default from server + response.""" + return response['user'].get(self.ID_KEY) + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + response = self.get_json(self.USER_INFO_URL, params={ + 'access_token': access_token, + 'compact': 'true' + }) + self.process_error(response) + + # The response data is buried in the 'response' key + return response['response'] From f4fa73e90730bb7d5ae530635bd1e59e2ad61572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 13 Apr 2016 14:03:18 -0300 Subject: [PATCH 788/890] v0.2.15 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++-- social/__init__.py | 2 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b977313..cf2d17e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Change Log +## [v0.2.15](https://github.com/omab/python-social-auth/tree/v0.2.15) (2016-04-13) + +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.14...v0.2.15) + +**Closed issues:** + +- Warning with dependency six [\#885](https://github.com/omab/python-social-auth/issues/885) +- Password Reset Emails don't come if Authenticated via Python Social Auth [\#881](https://github.com/omab/python-social-auth/issues/881) +- I followed the documentation, but it didn't work for me. Would you please let me know where my PIPELINE is wrong? [\#867](https://github.com/omab/python-social-auth/issues/867) +- Is this AttributeError caused by facebook settings or python-social-auth? [\#865](https://github.com/omab/python-social-auth/issues/865) +- Google: Backend not found [\#862](https://github.com/omab/python-social-auth/issues/862) +- Django 1.9.2 ImportError: No module named 'social.apps.django\_app' [\#861](https://github.com/omab/python-social-auth/issues/861) +- Microsoft live oauth sign up/sign in issue [\#837](https://github.com/omab/python-social-auth/issues/837) +- Redirect url always ends with /\#\_=\_ [\#833](https://github.com/omab/python-social-auth/issues/833) +- Google Sign in problem [\#826](https://github.com/omab/python-social-auth/issues/826) +- Fitbit oauth2 [\#733](https://github.com/omab/python-social-auth/issues/733) + +**Merged pull requests:** + +- Add a backend for Classlink. [\#890](https://github.com/omab/python-social-auth/pull/890) ([antinescience](https://github.com/antinescience)) +- Pass response to AuthCancel exception [\#883](https://github.com/omab/python-social-auth/pull/883) ([st4lk](https://github.com/st4lk)) +- modifed wrong key names in pocket.py [\#878](https://github.com/omab/python-social-auth/pull/878) ([EunJung-Seo](https://github.com/EunJung-Seo)) +- Fix typos [\#869](https://github.com/omab/python-social-auth/pull/869) ([Chronial](https://github.com/Chronial)) +- Do not instantiate Logger directly [\#864](https://github.com/omab/python-social-auth/pull/864) ([browniebroke](https://github.com/browniebroke)) +- Fix xgettext warning due to unknown encoding [\#856](https://github.com/omab/python-social-auth/pull/856) ([federicobond](https://github.com/federicobond)) +- Update base.py [\#852](https://github.com/omab/python-social-auth/pull/852) ([hellvix](https://github.com/hellvix)) +- Fix misspelled backend name [\#847](https://github.com/omab/python-social-auth/pull/847) ([victorgutemberg](https://github.com/victorgutemberg)) +- Add some tests for Spotify backend + add a backend for Deezer music service [\#845](https://github.com/omab/python-social-auth/pull/845) ([khamaileon](https://github.com/khamaileon)) +- \[Fix\] update odnoklasniki docs to new domain ok [\#836](https://github.com/omab/python-social-auth/pull/836) ([vanadium23](https://github.com/vanadium23)) +- add github enterprise docs on how to specify the API URL [\#834](https://github.com/omab/python-social-auth/pull/834) ([iserko](https://github.com/iserko)) +- Fix ImportError: cannot import name ‘urlencode’ in Python3 [\#828](https://github.com/omab/python-social-auth/pull/828) ([mishbahr](https://github.com/mishbahr)) +- Fix wrong evaluation of boolean kwargs [\#824](https://github.com/omab/python-social-auth/pull/824) ([falknes](https://github.com/falknes)) +- SAML: raise AuthMissingParameter if idp param missing [\#821](https://github.com/omab/python-social-auth/pull/821) ([omarkhan](https://github.com/omarkhan)) +- added support for ArcGIS OAuth2 [\#820](https://github.com/omab/python-social-auth/pull/820) ([aspcanada](https://github.com/aspcanada)) +- BaseOAuth2: Store access token in response if it does not exist [\#816](https://github.com/omab/python-social-auth/pull/816) ([kchange](https://github.com/kchange)) +- Minor backend fixes [\#815](https://github.com/omab/python-social-auth/pull/815) ([mback2k](https://github.com/mback2k)) +- Fix Django 1.10 Deprecation Warning "SubfieldBase has been deprecated." [\#813](https://github.com/omab/python-social-auth/pull/813) ([contracode](https://github.com/contracode)) +- Fix typo: "attacht he" -\> "attach the" [\#808](https://github.com/omab/python-social-auth/pull/808) ([smholloway](https://github.com/smholloway)) +- Azure AD updates [\#807](https://github.com/omab/python-social-auth/pull/807) ([vinhub](https://github.com/vinhub)) +- Remove unused response arg from user\_data method of yandex backend [\#784](https://github.com/omab/python-social-auth/pull/784) ([SrgyPetrov](https://github.com/SrgyPetrov)) +- Support all kind of data type \(like uuid\) of User.id on Pyramid [\#769](https://github.com/omab/python-social-auth/pull/769) ([cjltsod](https://github.com/cjltsod)) + ## [v0.2.14](https://github.com/omab/python-social-auth/tree/v0.2.14) (2016-01-25) [Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.13...v0.2.14) @@ -14,11 +56,13 @@ **Merged pull requests:** +- Add support for Drip Email Marketing Site [\#810](https://github.com/omab/python-social-auth/pull/810) ([buddylindsey](https://github.com/buddylindsey)) - Fix Django 1.10 deprecation warnings [\#806](https://github.com/omab/python-social-auth/pull/806) ([yprez](https://github.com/yprez)) - Changed instagram backend to new authorization routes [\#797](https://github.com/omab/python-social-auth/pull/797) ([clybob](https://github.com/clybob)) - Update settings.rst [\#793](https://github.com/omab/python-social-auth/pull/793) ([skolsuper](https://github.com/skolsuper)) - Add naver.com OAuth2 backend [\#789](https://github.com/omab/python-social-auth/pull/789) ([se0kjun](https://github.com/se0kjun)) - Formatter fixes for SAML to support Py2.6 [\#783](https://github.com/omab/python-social-auth/pull/783) ([matburt](https://github.com/matburt)) +- Add pinterest backend [\#774](https://github.com/omab/python-social-auth/pull/774) ([scailer](https://github.com/scailer)) - Fix typo [\#768](https://github.com/omab/python-social-auth/pull/768) ([mprunell](https://github.com/mprunell)) - Fixes a few grammar issues in the docs [\#764](https://github.com/omab/python-social-auth/pull/764) ([kevinharvey](https://github.com/kevinharvey)) - use qq openid as username [\#763](https://github.com/omab/python-social-auth/pull/763) ([lneoe](https://github.com/lneoe)) @@ -26,9 +70,11 @@ - Fix vk backend [\#757](https://github.com/omab/python-social-auth/pull/757) ([truetug](https://github.com/truetug)) - Fix odnoklassniki backend [\#756](https://github.com/omab/python-social-auth/pull/756) ([truetug](https://github.com/truetug)) - Store all tokens when tokens are refreshed [\#753](https://github.com/omab/python-social-auth/pull/753) ([mvschaik](https://github.com/mvschaik)) +- Added support for NGPVAN ActionID OpenID [\#750](https://github.com/omab/python-social-auth/pull/750) ([nickcatal](https://github.com/nickcatal)) - Python 3 support for facebook-app backend [\#749](https://github.com/omab/python-social-auth/pull/749) ([jhmaddox](https://github.com/jhmaddox)) - Save extra\_data on login [\#748](https://github.com/omab/python-social-auth/pull/748) ([mvschaik](https://github.com/mvschaik)) - Update URLs to match new site and remove OAuth comment. [\#744](https://github.com/omab/python-social-auth/pull/744) ([lukos](https://github.com/lukos)) +- Fitbit OAuth 2.0 support [\#743](https://github.com/omab/python-social-auth/pull/743) ([robbiet480](https://github.com/robbiet480)) - added AuthUnreachableProvider exception to documentation [\#729](https://github.com/omab/python-social-auth/pull/729) ([Qlio](https://github.com/Qlio)) - Add REDIRECT\_STATE = False [\#725](https://github.com/omab/python-social-auth/pull/725) ([webjunkie](https://github.com/webjunkie)) - Tuple in pipeline's documentation should be ended with coma [\#712](https://github.com/omab/python-social-auth/pull/712) ([JerzySpendel](https://github.com/JerzySpendel)) @@ -386,7 +432,7 @@ - Add Kakao link and detailed address for description. [\#403](https://github.com/omab/python-social-auth/pull/403) ([jeyraof](https://github.com/jeyraof)) - Added some legal stuff [\#402](https://github.com/omab/python-social-auth/pull/402) ([dzerrenner](https://github.com/dzerrenner)) - Recreate migration with Django 1.7 final and re-PEP8. [\#401](https://github.com/omab/python-social-auth/pull/401) ([akx](https://github.com/akx)) -- master add SCOPE\_SEPARATOR to DisqusOAuth2 [\#398](https://github.com/omab/python-social-auth/pull/398) ([ctrl-alt-delete](https://github.com/ctrl-alt-delete)) +- master add SCOPE\_SEPARATOR to DisqusOAuth2 [\#398](https://github.com/omab/python-social-auth/pull/398) ([vero4karu](https://github.com/vero4karu)) - added a backend for Battle.net Oauth2 auth [\#397](https://github.com/omab/python-social-auth/pull/397) ([dzerrenner](https://github.com/dzerrenner)) - Update documentation with info on upgrading from 0.1-0.2 with migrations [\#395](https://github.com/omab/python-social-auth/pull/395) ([timsavage](https://github.com/timsavage)) - Allow more Trello settings [\#389](https://github.com/omab/python-social-auth/pull/389) ([sk7](https://github.com/sk7)) @@ -920,4 +966,4 @@ -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/social/__init__.py b/social/__init__.py index e45dc94c7..4b3056ee9 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 14) +version = (0, 2, 15) extra = '' __version__ = '.'.join(map(str, version)) + extra From 3e55b45c8766f183c85b70cbbfa7bad1cf50c3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 13 Apr 2016 14:52:47 -0300 Subject: [PATCH 789/890] Docs updates --- docs/backends/arcgis.rst | 4 ++-- docs/backends/index.rst | 1 - docs/backends/pinterest.rst | 12 ++++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/backends/arcgis.rst b/docs/backends/arcgis.rst index 1ab43a271..b32578138 100644 --- a/docs/backends/arcgis.rst +++ b/docs/backends/arcgis.rst @@ -1,5 +1,5 @@ ArcGIS -===== +====== ArcGIS uses OAuth2 for authentication. @@ -7,7 +7,7 @@ ArcGIS uses OAuth2 for authentication. OAuth2 ------------- +------ 1. Add the OAuth2 backend to your settings page:: diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 97cf5fb85..7e3a23541 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -64,7 +64,6 @@ Social backends coinbase coursera dailymotion - deezer digitalocean disqus docker diff --git a/docs/backends/pinterest.rst b/docs/backends/pinterest.rst index d55be1c2f..7c18ab004 100644 --- a/docs/backends/pinterest.rst +++ b/docs/backends/pinterest.rst @@ -1,7 +1,7 @@ Pinterest ========= -Pinterest implemented OAuth2 protocol for their authentication mechanism. +Pinterest implemented OAuth2 protocol for their authentication mechanism. To enable ``python-social-auth`` support follow this steps: 1. Go to `Pinterest developers zone`_ and create an application. @@ -11,9 +11,9 @@ To enable ``python-social-auth`` support follow this steps: SOCIAL_AUTH_PINTEREST_KEY = '...' SOCIAL_AUTH_PINTEREST_SECRET = '...' SOCIAL_AUTH_PINTEREST_SCOPE = [ - 'read_public', - 'write_public', - 'read_relationships', + 'read_public', + 'write_public', + 'read_relationships', 'write_relationships' ] @@ -25,5 +25,5 @@ To enable ``python-social-auth`` support follow this steps: ... ) -.. _Pinterest Apps Console: https://developers.pinterest.com/apps/ -.. _Pinterest Documentation: https://developers.pinterest.com/docs/ +.. _Pinterest developers zone: https://developers.pinterest.com/apps/ +.. _Pinterest Documentation: https://developers.pinterest.com/docs/ From 879645a511ac71b143c76330cd7908f59d507509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 13 Apr 2016 14:55:29 -0300 Subject: [PATCH 790/890] v0.2.16 --- CHANGELOG.md | 4 +++- social/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2d17e69..c3b998865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Change Log -## [v0.2.15](https://github.com/omab/python-social-auth/tree/v0.2.15) (2016-04-13) +## [v0.2.16](https://github.com/omab/python-social-auth/tree/v0.2.16) (2016-04-13) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.15...v0.2.16) +## [v0.2.15](https://github.com/omab/python-social-auth/tree/v0.2.15) (2016-04-13) [Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.14...v0.2.15) **Closed issues:** diff --git a/social/__init__.py b/social/__init__.py index 4b3056ee9..281dab32e 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 15) +version = (0, 2, 16) extra = '' __version__ = '.'.join(map(str, version)) + extra From f974acf87152dc530f8658450c03d26c7d7568cc Mon Sep 17 00:00:00 2001 From: sbussetti Date: Wed, 13 Apr 2016 19:47:07 -0400 Subject: [PATCH 791/890] django 1.8+ compat to ensure to_python is always called when accessing result from db.. --- social/apps/django_app/default/fields.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py index 4a865eedc..46b134bb7 100644 --- a/social/apps/django_app/default/fields.py +++ b/social/apps/django_app/default/fields.py @@ -20,6 +20,9 @@ def __init__(self, *args, **kwargs): kwargs.setdefault('default', '{}') super(JSONField, self).__init__(*args, **kwargs) + def from_db_value(self, value, expression, connection, context): + return self.to_python(value) + def to_python(self, value): """ Convert the input JSON value into python structures, raises From 24661a7409828f6d158dabfb81c7247686e14325 Mon Sep 17 00:00:00 2001 From: duoduo369 Date: Tue, 5 Apr 2016 14:49:25 +0800 Subject: [PATCH 792/890] Add weixin public number oauth backend. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weixin have two oauth method: 1. User login with web, when user scan 2D barcode; 2. User click green submit button in Weixin app. These two way are very different in development scene. Beacause my poorly english and developer who use weixin oauth almost are Chinese.I'll explain in Chinese. 微信有两种授权方式: 1. 通过微信开放平台注册的账号,用户可以在web上点击用微信登陆,此时微信 会生成二维码,用户扫描后即可登陆; 2. 通过微信app登陆,开发者在微信开放平台注册微信公众号(服务号)后,用户 打开需要授权的页面后会先出现一个有绿色确认按钮的页面,当用户点击登陆后 完成登陆逻辑. 注意 1. 如果你是在两个平台注册的账号,用户体系需要用微信的uninionid自己 做唯一性关联,因为微信认为这是两个app即使你用同一个开放者账号注册;所以 现象是同一个用户用微信打开两个不同场景的服务登陆时,openid是不同的,但是 可以通过uninionid来关联,唯一确认用户。 2. 微信app的登陆方式请添加一个可以按照日期或者其他任何随机变化的参数, 因为微信对于转发有奇怪的限制,当转发量到一定程度后,微信会废弃这个链接, 现象是你自己在朋友圈可以看到自己转发的链接,但是你的朋友们却看不到。 --- social/backends/weixin.py | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/social/backends/weixin.py b/social/backends/weixin.py index 1b5c2d500..0eb81e5e6 100644 --- a/social/backends/weixin.py +++ b/social/backends/weixin.py @@ -3,6 +3,7 @@ """ Weixin OAuth2 backend """ +import urllib from requests import HTTPError from social.backends.oauth import BaseOAuth2 @@ -99,3 +100,74 @@ def auth_complete(self, *args, **kwargs): self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) + + +class WeixinOAuth2APP(WeixinOAuth2): + """Weixin OAuth authentication backend + + can't use in web, only in weixin app + """ + name = 'weixinapp' + ID_KEY = 'openid' + AUTHORIZATION_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + + def auth_url(self): + if self.STATE_PARAMETER or self.REDIRECT_STATE: + # Store state in session for further request validation. The state + # value is passed as state parameter (as specified in OAuth2 spec), + # but also added to redirect, that way we can still verify the + # request if the provider doesn't implement the state parameter. + # Reuse token if any. + name = self.name + '_state' + state = self.strategy.session_get(name) + if state is None: + state = self.state_token() + self.strategy.session_set(name, state) + else: + state = None + + params = self.auth_params(state) + params.update(self.get_scope_argument()) + params.update(self.auth_extra_arguments()) + params = urllib.urlencode(sorted(params.items())) + return '{}#wechat_redirect'.format(self.AUTHORIZATION_URL + '?' + params) + + + def auth_complete_params(self, state=None): + appid, secret = self.get_key_and_secret() + return { + 'grant_type': 'authorization_code', # request auth code + 'code': self.data.get('code', ''), # server response code + 'appid': appid, + 'secret': secret, + } + + def validate_state(self): + return None + + def auth_complete(self, *args, **kwargs): + """Completes loging process, must return user instance""" + self.process_error(self.data) + try: + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) + except HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(self) + else: + raise + except KeyError: + raise AuthUnknownError(self) + + if 'errcode' in response: + raise AuthCanceled(self) + self.process_error(response) + return self.do_auth(response['access_token'], response=response, + *args, **kwargs) From d8501cd6dbad8481cc67ec0aadea2b2d97cfe4af Mon Sep 17 00:00:00 2001 From: Scott Percival Date: Fri, 15 Apr 2016 15:55:33 +0800 Subject: [PATCH 793/890] BaseStrategy.validate_email: add missing email address check --- social/strategies/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social/strategies/base.py b/social/strategies/base.py index ce66af09e..22f24e195 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -131,6 +131,8 @@ def validate_email(self, email, code): verification_code = self.storage.code.get_code(code) if not verification_code or verification_code.code != code: return False + elif verification_code.email != email: + return False else: verification_code.verify() return True From 8f8de02d7c6912847b8b092940ab957f5bf0a1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 15 Apr 2016 13:29:15 -0300 Subject: [PATCH 794/890] Ditch Python 2.6, bump up requirements --- .travis.yml | 1 - requirements-python3.txt | 12 ++++++------ requirements.txt | 12 ++++++------ run_tox.sh | 4 +--- setup.py | 1 - social/tests/__init__.py | 9 --------- tox.ini | 2 +- 7 files changed, 14 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc67088c5..b6841d41d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ env: - REQUIREMENTS=requirements.txt - TEST_REQUIREMENTS=social/tests/requirements.txt python: - - "2.6" - "2.7" matrix: include: diff --git a/requirements-python3.txt b/requirements-python3.txt index 7a856dfd9..b834c7f82 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -1,6 +1,6 @@ -python3-openid>=3.0.1 -requests>=1.1.0 -oauthlib>=0.3.8 -requests-oauthlib>0.3.2 -six>=1.2.0 -PyJWT>=1.0.0 +python3-openid>=3.0.9 +requests>=2.9.1 +oauthlib>=1.0.3 +requests-oauthlib>=0.6.1 +six>=1.10.0 +PyJWT>=1.4.0 diff --git a/requirements.txt b/requirements.txt index 1d05eedeb..aa94fffd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -python-openid>=2.2 -requests>=2.5.1 -oauthlib>=0.3.8 -requests-oauthlib>=0.3.1 -six>=1.2.0 -PyJWT>=1.0.0 +python-openid>=2.2.5 +requests>=2.9.1 +oauthlib>=1.0.3 +requests-oauthlib>=0.6.1 +six>=1.10.0 +PyJWT>=1.4.0 diff --git a/run_tox.sh b/run_tox.sh index b38f764e6..55d0d6b2b 100755 --- a/run_tox.sh +++ b/run_tox.sh @@ -2,16 +2,14 @@ # 1. Install pyenv # 2. Install python versions -# pyenv install 2.6.9 # pyenv install 2.7.11 # pyenv install 3.3.6 # pyenv install 3.4.4 # pyenv install pypy-4.0.1 # 3. Switch to each version and install / update setuptools, pip, tox -# pyenv local 2.6.9 # pip install -U setuptools pip tox # 4. Enable versions -# pyenv local 2.6.9 2.7.11 3.3.6 3.4.4 pypy-4.0.1 +# pyenv local 2.7.11 3.3.6 3.4.4 pypy-4.0.1 # 5. Run tox which pyenv && eval "$(pyenv init -)" diff --git a/setup.py b/setup.py index a706a77cd..41282fcad 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,6 @@ def get_packages(): 'Intended Audience :: Developers', 'Environment :: Web Environment', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3' ], diff --git a/social/tests/__init__.py b/social/tests/__init__.py index 46c7ced67..e69de29bb 100644 --- a/social/tests/__init__.py +++ b/social/tests/__init__.py @@ -1,9 +0,0 @@ -import sys -import warnings - - -# Ignore deprecation warnings on Python2.6. Maybe it's time to ditch this -# oldie? -if sys.version_info[0] == 2 and sys.version_info[1] == 6 or \ - hasattr(sys, 'pypy_version_info'): - warnings.filterwarnings('ignore', category=Warning) diff --git a/tox.ini b/tox.ini index 57eec3416..3d1952b57 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py33, py34, pypy, doc +envlist = py27, py33, py34, pypy, doc [testenv] commands = nosetests --where=social/tests --stop From 78182009603e37b3af77748687c98c5c2d84b92b Mon Sep 17 00:00:00 2001 From: Sylvain Zimmer Date: Fri, 15 Apr 2016 15:12:16 -0400 Subject: [PATCH 795/890] Added Sketchfab OAuth2 backend --- README.rst | 2 + docs/backends/index.rst | 1 + docs/backends/sketchfab.rst | 15 ++++++++ examples/django_example/example/settings.py | 1 + .../django_me_example/example/settings.py | 1 + social/apps/django_app/tests.py | 1 + social/backends/sketchfab.py | 38 +++++++++++++++++++ social/tests/backends/test_sketchfab.py | 26 +++++++++++++ 8 files changed, 85 insertions(+) create mode 100644 docs/backends/sketchfab.rst create mode 100644 social/backends/sketchfab.py create mode 100644 social/tests/backends/test_sketchfab.py diff --git a/README.rst b/README.rst index ec4e25a60..87efeadb5 100644 --- a/README.rst +++ b/README.rst @@ -108,6 +108,7 @@ or current ones extended): * Readability_ OAuth1 * Reddit_ OAuth2 https://github.com/reddit/reddit/wiki/OAuth2 * Shopify_ OAuth2 + * Sketchfab_ OAuth2 * Skyrock_ OAuth1 * Soundcloud_ OAuth2 * Stackoverflow_ OAuth2 @@ -278,6 +279,7 @@ check `django-social-auth LICENSE`_ for details: .. _Pocket: http://getpocket.com .. _Podio: https://podio.com .. _Shopify: http://shopify.com +.. _Sketchfab: https://sketchfab.com/developers/oauth .. _Skyrock: https://skyrock.com .. _Soundcloud: https://soundcloud.com .. _Stocktwits: https://stocktwits.com diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 7e3a23541..bb6bdc35d 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -120,6 +120,7 @@ Social backends runkeeper salesforce shopify + sketchfab skyrock slack soundcloud diff --git a/docs/backends/sketchfab.rst b/docs/backends/sketchfab.rst new file mode 100644 index 000000000..cedaa66f0 --- /dev/null +++ b/docs/backends/sketchfab.rst @@ -0,0 +1,15 @@ +Sketchfab +========= + +Sketchfab uses OAuth 2 for authentication. + +To use: + +- Follow the steps at `Sketchfab Oauth`_ + +- Fill the ``Client id/key`` and ``Client Secret`` values you received in your django settings:: + + SOCIAL_AUTH_SKETCHFAB_KEY = '' + SOCIAL_AUTH_SKETCHFAB_SECRET = '' + +.. _Sketchfab Oauth: https://sketchfab.com/developers/oauth \ No newline at end of file diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 62f2b5b2a..955848749 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -174,6 +174,7 @@ 'social.backends.readability.ReadabilityOAuth', 'social.backends.reddit.RedditOAuth2', 'social.backends.runkeeper.RunKeeperOAuth2', + 'social.backends.sketchfab.SketchfabOAuth2', 'social.backends.skyrock.SkyrockOAuth', 'social.backends.soundcloud.SoundcloudOAuth2', 'social.backends.spotify.SpotifyOAuth2', diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 7dcdfad24..b25968a0f 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -173,6 +173,7 @@ 'social.backends.yammer.YammerOAuth2', 'social.backends.stackoverflow.StackoverflowOAuth2', 'social.backends.readability.ReadabilityOAuth', + 'social.backends.sketchfab.SketchfabOAuth2', 'social.backends.skyrock.SkyrockOAuth', 'social.backends.tumblr.TumblrOAuth', 'social.backends.reddit.RedditOAuth2', diff --git a/social/apps/django_app/tests.py b/social/apps/django_app/tests.py index 022d7fc6e..1dec95ef7 100644 --- a/social/apps/django_app/tests.py +++ b/social/apps/django_app/tests.py @@ -31,6 +31,7 @@ from social.tests.backends.test_podio import * from social.tests.backends.test_readability import * from social.tests.backends.test_reddit import * +from social.tests.backends.test_sketchfab import * from social.tests.backends.test_skyrock import * from social.tests.backends.test_soundcloud import * from social.tests.backends.test_stackoverflow import * diff --git a/social/backends/sketchfab.py b/social/backends/sketchfab.py new file mode 100644 index 000000000..76140413c --- /dev/null +++ b/social/backends/sketchfab.py @@ -0,0 +1,38 @@ +""" +Sketchfab OAuth2 backend, docs at: + http://psa.matiasaguirre.net/docs/backends/sketchfab.html + https://sketchfab.com/developers/oauth +""" +from social.backends.oauth import BaseOAuth2 + + +class SketchfabOAuth2(BaseOAuth2): + name = 'sketchfab' + ID_KEY = 'uid' + AUTHORIZATION_URL = 'https://sketchfab.com/oauth2/authorize/' + ACCESS_TOKEN_URL = 'https://sketchfab.com/oauth2/token/' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + REQUIRES_EMAIL_VALIDATION = False + EXTRA_DATA = [ + ('username', 'username'), + ('apiToken', 'apiToken') + ] + + def get_user_details(self, response): + """Return user details from Sketchfab account""" + user_data = response + email = user_data.get('email', '') + username = user_data['username'] + name = user_data.get('displayName', '') + fullname, first_name, last_name = self.get_user_names(name) + return {'username': username, + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name, + 'email': email} + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json('https://sketchfab.com/v2/users/me', + headers={'Authorization': 'Bearer {0}'.format(access_token)}) diff --git a/social/tests/backends/test_sketchfab.py b/social/tests/backends/test_sketchfab.py new file mode 100644 index 000000000..ada632fb4 --- /dev/null +++ b/social/tests/backends/test_sketchfab.py @@ -0,0 +1,26 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class SketchfabOAuth2Test(OAuth2Test): + backend_path = 'social.backends.sketchfab.SketchfabOAuth2' + user_data_url = 'https://sketchfab.com/v2/users/me' + expected_username = 'foobar' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'uid': '42', + 'email': 'foo@bar.com', + 'displayName': 'foo bar', + 'username': 'foobar', + 'apiToken': 'XXX' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From ee21cbc6215a8dd2c927d6190ee9828751426630 Mon Sep 17 00:00:00 2001 From: Sylvain Zimmer Date: Fri, 15 Apr 2016 19:09:56 -0400 Subject: [PATCH 796/890] Doc improvement --- docs/backends/sketchfab.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/sketchfab.rst b/docs/backends/sketchfab.rst index cedaa66f0..865b8c483 100644 --- a/docs/backends/sketchfab.rst +++ b/docs/backends/sketchfab.rst @@ -5,7 +5,7 @@ Sketchfab uses OAuth 2 for authentication. To use: -- Follow the steps at `Sketchfab Oauth`_ +- Follow the steps at `Sketchfab Oauth`_, and ask for an ``Authorization code`` grant type. - Fill the ``Client id/key`` and ``Client Secret`` values you received in your django settings:: From f31ff051ff4ea7c1ff673cd0bcb987fe03db81c8 Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 05:50:43 +0300 Subject: [PATCH 797/890] ADDED: backend for Upwork Signed-off-by: Shepilov Vladislav --- social/backends/upwork.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 social/backends/upwork.py diff --git a/social/backends/upwork.py b/social/backends/upwork.py new file mode 100644 index 000000000..f7dcdc249 --- /dev/null +++ b/social/backends/upwork.py @@ -0,0 +1,35 @@ +""" +Upwork OAuth1 backend +""" +from social.backends.oauth import BaseOAuth1 + + +class UpworkOAuth(BaseOAuth1): + """Upwork OAuth authentication backend""" + name = 'upwork' + ID_KEY = 'id' + AUTHORIZATION_URL = 'https://www.upwork.com/services/api/auth' + REQUEST_TOKEN_URL = 'https://www.upwork.com/api/auth/v1/oauth/token/request' + REQUEST_TOKEN_METHOD = 'POST' + ACCESS_TOKEN_URL = 'https://www.upwork.com/api/auth/v1/oauth/token/access' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' + + def get_user_details(self, response): + """Return user details from Upwork account""" + auth_user = response.get('auth_user', {}) + first_name = auth_user.get('first_name') + last_name = auth_user.get('last_name') + fullname = '{} {}'.format(first_name, last_name) + return { + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + return self.get_json( + 'https://www.upwork.com/api/auth/v1/info.json', + auth=self.oauth_auth(access_token) + ) From 7af138e84cb80152eab2b8f711d80ae46fd6636e Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 05:50:57 +0300 Subject: [PATCH 798/890] ADDED: test fot upwork backend Signed-off-by: Shepilov Vladislav --- social/tests/backends/test_upwork.py | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 social/tests/backends/test_upwork.py diff --git a/social/tests/backends/test_upwork.py b/social/tests/backends/test_upwork.py new file mode 100644 index 000000000..8eb6d8ddc --- /dev/null +++ b/social/tests/backends/test_upwork.py @@ -0,0 +1,53 @@ +import json + +from social.p3 import urlencode +from social.tests.backends.oauth import OAuth1Test + + +class UpworkOAuth1Test(OAuth1Test): + backend_path = 'social.backends.upwork.UpworkOAuth' + user_data_url = 'https://www.upwork.com/api/auth/v1/info.json' + expected_username = '10101010' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + request_token_body = urlencode({ + 'oauth_token_secret': 'foobar-secret', + 'oauth_token': 'foobar', + 'oauth_callback_confirmed': 'true' + }) + user_data_body = json.dumps({ + 'info': { + 'portrait_32_img': '', + 'capacity': { + 'buyer': 'no', + 'affiliate_manager': 'no', + 'provider': 'yes' + }, + 'company_url': '', + 'has_agency': '1', + 'portrait_50_img': '', + 'portrait_100_img': '', + 'location': { + 'city': 'New York', + 'state': '', + 'country': 'USA' + }, + 'ref': '9755314', + 'profile_url': 'https://www.upwork.com/users/~10101010' + }, + 'auth_user': { + 'timezone': 'USA/New York', + 'first_name': 'Foo', + 'last_name': 'Bar', + 'timezone_offset': '10000' + }, + 'server_time': '1111111111' + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From efe6d7c0f590b5cee3d6ba44481e118317a74112 Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 05:53:49 +0300 Subject: [PATCH 799/890] ADDED: 'social.backends.upwork.UpworkOAuth' to settings for examples Signed-off-by: Shepilov Vladislav --- examples/cherrypy_example/local_settings.py.template | 1 + examples/django_example/example/settings.py | 1 + examples/django_me_example/example/settings.py | 1 + examples/flask_example/settings.py | 1 + examples/flask_me_example/settings.py | 1 + examples/pyramid_example/example/settings.py | 1 + examples/tornado_example/settings.py | 1 + examples/webpy_example/app.py | 1 + 8 files changed, 8 insertions(+) diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template index 98c8f8f26..7209aca8d 100644 --- a/examples/cherrypy_example/local_settings.py.template +++ b/examples/cherrypy_example/local_settings.py.template @@ -39,6 +39,7 @@ SOCIAL_SETTINGS = { 'social.backends.podio.PodioOAuth2', 'social.backends.reddit.RedditOAuth2', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', ), 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': '', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': '' diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py index 62f2b5b2a..e829f7144 100644 --- a/examples/django_example/example/settings.py +++ b/examples/django_example/example/settings.py @@ -203,6 +203,7 @@ 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', + 'social.backends.upwork.UpworkOAuth', ) AUTH_USER_MODEL = 'app.CustomUser' diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py index 7dcdfad24..bd1f46b03 100644 --- a/examples/django_me_example/example/settings.py +++ b/examples/django_me_example/example/settings.py @@ -182,6 +182,7 @@ 'social.backends.email.EmailAuth', 'social.backends.username.UsernameAuth', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', 'mongoengine.django.auth.MongoEngineBackend', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py index 0abaa915c..6b5324cc5 100644 --- a/examples/flask_example/settings.py +++ b/examples/flask_example/settings.py @@ -52,4 +52,5 @@ 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', ) diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py index e4f2338ac..cf4ed21e9 100644 --- a/examples/flask_me_example/settings.py +++ b/examples/flask_me_example/settings.py @@ -58,4 +58,5 @@ 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', ) diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py index 35a69ade1..05e2d9eef 100644 --- a/examples/pyramid_example/example/settings.py +++ b/examples/pyramid_example/example/settings.py @@ -46,6 +46,7 @@ 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', ) } diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py index ff6b14684..81ae2eb5d 100644 --- a/examples/tornado_example/settings.py +++ b/examples/tornado_example/settings.py @@ -45,6 +45,7 @@ 'social.backends.reddit.RedditOAuth2', 'social.backends.mineid.MineIDOAuth2', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', ) from local_settings import * diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py index 4109fac46..2224d93ec 100644 --- a/examples/webpy_example/app.py +++ b/examples/webpy_example/app.py @@ -57,6 +57,7 @@ 'social.backends.podio.PodioOAuth2', 'social.backends.mineid.MineIDOAuth2', 'social.backends.wunderlist.WunderlistOAuth2', + 'social.backends.upwork.UpworkOAuth', ) web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/' From 9e8710e0951935575627b2d97bf869f01a9227e8 Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 06:01:03 +0300 Subject: [PATCH 800/890] ADDED: upwork backend doc Signed-off-by: Shepilov Vladislav --- docs/backends/upwork.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/backends/upwork.rst diff --git a/docs/backends/upwork.rst b/docs/backends/upwork.rst new file mode 100644 index 000000000..0239e20fd --- /dev/null +++ b/docs/backends/upwork.rst @@ -0,0 +1,28 @@ +Upwork +======= + +Upwork supports only OAuth 1. + +- Register a new application at `Upwork Developers`_. + +OAuth1 +------ + +Add the Upwork OAuth backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.upwork.UpworkOAuth', + ... + ) + +- Fill ``App Key`` and ``App Secret`` values in the settings:: + + SOCIAL_AUTH_UPWORK_KEY = '' + SOCIAL_AUTH_UPWORK_SECRET = '' + +------ +For more information please go to `Upwork API Reference`_. + +.. Upwork Developers: https://www.upwork.com/services/api/apply +.. Upwork API Reference: https://developers.upwork.com/?lang=python From e2516389e5db257102628163b2909a85a8f0a010 Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 06:03:46 +0300 Subject: [PATCH 801/890] UPDATE: upwork doc Signed-off-by: Shepilov Vladislav --- docs/backends/upwork.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backends/upwork.rst b/docs/backends/upwork.rst index 0239e20fd..338d009da 100644 --- a/docs/backends/upwork.rst +++ b/docs/backends/upwork.rst @@ -24,5 +24,5 @@ Add the Upwork OAuth backend to your settings page:: ------ For more information please go to `Upwork API Reference`_. -.. Upwork Developers: https://www.upwork.com/services/api/apply -.. Upwork API Reference: https://developers.upwork.com/?lang=python +.. _Upwork Developers: https://www.upwork.com/services/api/apply +.. _Upwork API Reference: https://developers.upwork.com/?lang=python From 564fb41abaaa34375a57ec1b9068eb53ef75bbad Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 06:08:59 +0300 Subject: [PATCH 802/890] FIXED: site index linking Signed-off-by: Shepilov Vladislav --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 7e3a23541..43a9d5e31 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -139,6 +139,7 @@ Social backends twitch twitter uber + upwork vend vimeo vk From 4dd27dae5004af752af64185c00ed04b1ac223b2 Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 06:12:10 +0300 Subject: [PATCH 803/890] ADDED: upwork to intro docs Signed-off-by: Shepilov Vladislav --- docs/intro.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/intro.rst b/docs/intro.rst index 12a45cb70..af3e37c2e 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -83,6 +83,7 @@ or extend current one): * Twilio_ Auth * Twitch_ OAuth2 * Twitter_ OAuth1 + * Upwork_ OAuth1 * Vimeo_ OAuth1 * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications * Weibo_ OAuth2 @@ -179,3 +180,4 @@ section. .. _Webpy: https://github.com/omab/python-social-auth/tree/master/social/apps/webpy_app .. _Tornado: http://www.tornadoweb.org/ .. _Authentication Pipeline: pipeline.html +.. _Upwork: https://www.upwork.com From 3ae8e70294e8a60dc4a03b6f9172e0bc3c22e6df Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Wed, 20 Apr 2016 06:15:29 +0300 Subject: [PATCH 804/890] FIXED: docs Signed-off-by: Shepilov Vladislav --- docs/backends/upwork.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backends/upwork.rst b/docs/backends/upwork.rst index 338d009da..8e5d2c820 100644 --- a/docs/backends/upwork.rst +++ b/docs/backends/upwork.rst @@ -21,8 +21,8 @@ Add the Upwork OAuth backend to your settings page:: SOCIAL_AUTH_UPWORK_KEY = '' SOCIAL_AUTH_UPWORK_SECRET = '' ------- -For more information please go to `Upwork API Reference`_. + +**Note:** For more information please go to `Upwork API Reference`_. .. _Upwork Developers: https://www.upwork.com/services/api/apply .. _Upwork API Reference: https://developers.upwork.com/?lang=python From b87ef654b55f279ee9a6989280000e666a1827e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 20 Apr 2016 13:39:54 -0300 Subject: [PATCH 805/890] v0.2.17 --- CHANGELOG.md | 8 ++++++++ social/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b998865..f65d2a8af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [v0.2.17](https://github.com/omab/python-social-auth/tree/v0.2.17) (2016-04-20) + +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.16...v0.2.17) + +**Merged pull requests:** + +- django 1.8+ compat to ensure to\_python is always called when accessing result from db.. [\#897](https://github.com/omab/python-social-auth/pull/897) ([sbussetti](https://github.com/sbussetti)) + ## [v0.2.16](https://github.com/omab/python-social-auth/tree/v0.2.16) (2016-04-13) [Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.15...v0.2.16) diff --git a/social/__init__.py b/social/__init__.py index 281dab32e..9d8e0634d 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 16) +version = (0, 2, 17) extra = '' __version__ = '.'.join(map(str, version)) + extra From 7edf3c72ae932da0108aff4c78d341bb203d230b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Wed, 20 Apr 2016 13:42:01 -0300 Subject: [PATCH 806/890] v0.2.18 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 9d8e0634d..c13010fe4 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 17) +version = (0, 2, 18) extra = '' __version__ = '.'.join(map(str, version)) + extra From 316d0c8f68233669cdf0c3f1d389f3f74baec49c Mon Sep 17 00:00:00 2001 From: slushkovsky Date: Fri, 22 Apr 2016 00:42:44 +0300 Subject: [PATCH 807/890] Update vk.rst I got it only then I've opened sources. In my opinion it will be helpful to be here, because settings without "SOCIAL_AUTH_" prifix uses only inside modules, not on web wramework config level. --- docs/backends/vk.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/vk.rst b/docs/backends/vk.rst index 2b5deb57a..a9195c76b 100644 --- a/docs/backends/vk.rst +++ b/docs/backends/vk.rst @@ -15,7 +15,7 @@ VK.com uses OAuth2 for Authentication. SOCIAL_AUTH_VK_OAUTH2_KEY = '' SOCIAL_AUTH_VK_OAUTH2_SECRET = '' -- Add ``'social.backends.vk.VKOAuth2'`` into your ``AUTHENTICATION_BACKENDS``. +- Add ``'social.backends.vk.VKOAuth2'`` into your ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - Then you can start using ``/login/vk-oauth2`` in your link href. From b3ef827c7c8dc37484654e1923afd4c2eaec18f6 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Sat, 23 Apr 2016 00:09:53 -0400 Subject: [PATCH 808/890] Corrected default value of JSONField The default should be an empty dict, not a string value. Fixes #898 --- social/apps/django_app/default/fields.py | 2 +- .../migrations/0004_auto_20160423_0400.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 social/apps/django_app/default/migrations/0004_auto_20160423_0400.py diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py index 46b134bb7..859338643 100644 --- a/social/apps/django_app/default/fields.py +++ b/social/apps/django_app/default/fields.py @@ -17,7 +17,7 @@ class JSONField(models.TextField): """ def __init__(self, *args, **kwargs): - kwargs.setdefault('default', '{}') + kwargs.setdefault('default', {}) super(JSONField, self).__init__(*args, **kwargs) def from_db_value(self, value, expression, connection, context): diff --git a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py new file mode 100644 index 000000000..668bf0e3d --- /dev/null +++ b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import social.apps.django_app.default.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('default', '0003_alter_email_max_length'), + ] + + operations = [ + migrations.AlterField( + model_name='usersocialauth', + name='extra_data', + field=social.apps.django_app.default.fields.JSONField(default={}), + ), + ] From 0233d9b21648f330b608e6025a015ec6e773132a Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Sat, 23 Apr 2016 17:30:17 -0400 Subject: [PATCH 809/890] Added support for passing kwargs to jwt.decode() when using OpenIdConnectAuth --- social/backends/open_id.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 3ab525257..a6c612488 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -325,13 +325,19 @@ def validate_and_return_id_token(self, id_token): http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. """ client_id, _client_secret = self.get_key_and_secret() - decryption_key = self.setting('ID_TOKEN_DECRYPTION_KEY') + + decode_kwargs = { + 'algorithms': ['HS256'], + 'audience': client_id, + 'issuer': self.ID_TOKEN_ISSUER, + 'key': self.setting('ID_TOKEN_DECRYPTION_KEY'), + } + decode_kwargs.update(self.setting('ID_TOKEN_JWT_DECODE_KWARGS', {})) + try: # Decode the JWT and raise an error if the secret is invalid or # the response has expired. - id_token = jwt_decode(id_token, decryption_key, audience=client_id, - issuer=self.ID_TOKEN_ISSUER, - algorithms=['HS256']) + id_token = jwt_decode(id_token, **decode_kwargs) except InvalidTokenError as err: raise AuthTokenError(self, err) From 8b00dd1edb27b7918ab50bedb3b2d878e5941dc8 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Sat, 23 Apr 2016 18:30:25 -0400 Subject: [PATCH 810/890] Storing token_type in extra_data field when using OAuth 2.0 This value is useful for authentication servers that may provide multiple types of tokens. --- social/backends/oauth.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index b2210ec0b..6b3b6a3d6 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -357,6 +357,13 @@ def auth_headers(self): return {'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'} + def extra_data(self, user, uid, response, details=None, *args, **kwargs): + """Return access_token, token_type, and extra defined names to store in + extra_data field""" + data = super(BaseOAuth2, self).extra_data(user, uid, response, details=details, *args, **kwargs) + data['token_type'] = response.get('token_type') or kwargs.get('token_type') + return data + def request_access_token(self, *args, **kwargs): return self.get_json(*args, **kwargs) From d5b018382e02fd78957fa42285b42700caa319bf Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Sat, 23 Apr 2016 17:35:40 -0400 Subject: [PATCH 811/890] Updated ID token iat claim validation for OpenIdConnectAuth The maximum age of the ID token is now exposed as a class variable rather than hardcoded to 10 minutes. --- social/backends/open_id.py | 25 +++++++++++++++++-------- social/tests/backends/open_id.py | 25 ++++++++++++------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index a6c612488..91cd39e80 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -2,18 +2,16 @@ from calendar import timegm from jwt import InvalidTokenError, decode as jwt_decode - from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE from openid.consumer.discover import DiscoveryFailure from openid.extensions import sreg, ax, pape -from social.utils import url_add_parameters -from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthMissingParameter, \ - AuthTokenError from social.backends.base import BaseAuth from social.backends.oauth import BaseOAuth2 - +from social.exceptions import ( + AuthException, AuthFailed, AuthCanceled, AuthUnknownError, AuthMissingParameter, AuthTokenError +) +from social.utils import url_add_parameters # OpenID configuration OLD_AX_ATTRS = [ @@ -278,6 +276,7 @@ class OpenIdConnectAuth(BaseOAuth2): Currently only the code response type is supported. """ ID_TOKEN_ISSUER = None + ID_TOKEN_MAX_AGE = 600 DEFAULT_SCOPE = ['openid'] EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')] # Set after access_token is retrieved @@ -331,6 +330,15 @@ def validate_and_return_id_token(self, id_token): 'audience': client_id, 'issuer': self.ID_TOKEN_ISSUER, 'key': self.setting('ID_TOKEN_DECRYPTION_KEY'), + 'options': { + 'verify_signature': True, + 'verify_exp': True, + 'verify_iat': True, + 'verify_aud': True, + 'verify_iss': True, + 'require_exp': True, + 'require_iat': True, + }, } decode_kwargs.update(self.setting('ID_TOKEN_JWT_DECODE_KWARGS', {})) @@ -341,9 +349,10 @@ def validate_and_return_id_token(self, id_token): except InvalidTokenError as err: raise AuthTokenError(self, err) - # Verify the token was issued in the last 10 minutes + # Verify the token was issued within a specified amount of time + iat_leeway = self.setting('ID_TOKEN_MAX_AGE', self.ID_TOKEN_MAX_AGE) utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) - if id_token['iat'] < (utc_timestamp - 600): + if id_token['iat'] < (utc_timestamp - iat_leeway): raise AuthTokenError(self, 'Incorrect id_token: iat') # Validate the nonce to ensure the request was not modified diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index 22e6d45e3..c46a550d1 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -from calendar import timegm - -import sys -import json import datetime +import json +import sys +from calendar import timegm -import requests import jwt - +import requests from openid import oidutil @@ -127,14 +125,15 @@ class OpenIdConnectTestMixin(object): client_key = 'a-key' client_secret = 'a-secret-key' issuer = None # id_token issuer + id_token_max_age = 600 # seconds def extra_settings(self): settings = super(OpenIdConnectTestMixin, self).extra_settings() settings.update({ 'SOCIAL_AUTH_{0}_KEY'.format(self.name): self.client_key, 'SOCIAL_AUTH_{0}_SECRET'.format(self.name): self.client_secret, - 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): - self.client_secret + 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): self.client_secret, + 'SOCIAL_AUTH_{0}_ID_TOKEN_MAX_AGE'.format(self.name): self.id_token_max_age, }) return settings @@ -197,11 +196,11 @@ def prepare_access_token_body(self, client_key=None, client_secret=None, algorithm='HS256').decode('utf-8') return json.dumps(body) - def authtoken_raised(self, expected_message, **access_token_kwargs): + def authtoken_raised(self, expected_message_regexp, **access_token_kwargs): self.access_token_body = self.prepare_access_token_body( **access_token_kwargs ) - with self.assertRaisesRegexp(AuthTokenError, expected_message): + with self.assertRaisesRegexp(AuthTokenError, expected_message_regexp): self.do_login() def test_invalid_secret(self): @@ -225,10 +224,10 @@ def test_invalid_audience(self): client_key='someone-else') def test_invalid_issue_time(self): - expiration_datetime = datetime.datetime.utcnow() - \ - datetime.timedelta(hours=1) + issue_datetime = datetime.datetime.utcnow() - \ + datetime.timedelta(seconds=self.id_token_max_age + 1) self.authtoken_raised('Token error: Incorrect id_token: iat', - issue_datetime=expiration_datetime) + issue_datetime=issue_datetime) def test_invalid_nonce(self): self.authtoken_raised( From 344f61e254a4d412d66c89f7c986a5e3fa988120 Mon Sep 17 00:00:00 2001 From: ccs Date: Tue, 26 Apr 2016 09:19:22 -0400 Subject: [PATCH 812/890] Add AppConfig Label of "social_auth" for migrations --- social/apps/django_app/default/config.py | 1 + .../apps/django_app/default/migrations/0002_add_related_name.py | 2 +- .../default/migrations/0003_alter_email_max_length.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/social/apps/django_app/default/config.py b/social/apps/django_app/default/config.py index 745e24d9c..a2c44b727 100644 --- a/social/apps/django_app/default/config.py +++ b/social/apps/django_app/default/config.py @@ -3,6 +3,7 @@ class PythonSocialAuthConfig(AppConfig): name = 'social.apps.django_app.default' + label = 'social_auth' verbose_name = 'Python Social Auth' def ready(self): diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py index 8e39f15bf..00f863d3c 100644 --- a/social/apps/django_app/default/migrations/0002_add_related_name.py +++ b/social/apps/django_app/default/migrations/0002_add_related_name.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('default', '0001_initial'), + ('social_auth', '0001_initial'), ] operations = [ diff --git a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py index 882c3112e..2f5c86ea3 100644 --- a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py +++ b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ - ('default', '0002_add_related_name'), + ('social_auth', '0002_add_related_name'), ] operations = [ From 5aee1f813322578eefb031282bb214915ce44052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 26 Apr 2016 14:00:50 -0300 Subject: [PATCH 813/890] PEP8 --- social/backends/open_id.py | 10 ++++++---- social/tests/backends/open_id.py | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 91cd39e80..1b525eb22 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -2,16 +2,18 @@ from calendar import timegm from jwt import InvalidTokenError, decode as jwt_decode + from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE from openid.consumer.discover import DiscoveryFailure from openid.extensions import sreg, ax, pape +from social.utils import url_add_parameters from social.backends.base import BaseAuth from social.backends.oauth import BaseOAuth2 -from social.exceptions import ( - AuthException, AuthFailed, AuthCanceled, AuthUnknownError, AuthMissingParameter, AuthTokenError -) -from social.utils import url_add_parameters +from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ + AuthUnknownError, AuthMissingParameter, \ + AuthTokenError + # OpenID configuration OLD_AX_ATTRS = [ diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index c46a550d1..ead0278e0 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -132,8 +132,10 @@ def extra_settings(self): settings.update({ 'SOCIAL_AUTH_{0}_KEY'.format(self.name): self.client_key, 'SOCIAL_AUTH_{0}_SECRET'.format(self.name): self.client_secret, - 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): self.client_secret, - 'SOCIAL_AUTH_{0}_ID_TOKEN_MAX_AGE'.format(self.name): self.id_token_max_age, + 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): + self.client_secret, + 'SOCIAL_AUTH_{0}_ID_TOKEN_MAX_AGE'.format(self.name): + self.id_token_max_age }) return settings From c1fcb6ab765b49456937af6efb9f4365501cbe35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 Apr 2016 13:39:39 -0300 Subject: [PATCH 814/890] Exclude site from package --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 365848c3c..d81798529 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [flake8] max-line-length = 119 # Ignore some well known paths -exclude = .venv,.tox,dist,doc,build,*.egg,db/env.py,db/versions/*.py +exclude = .venv,.tox,dist,doc,build,*.egg,db/env.py,db/versions/*.py,site [nosetests] verbosity=2 From 3a2e40c1d4341a0237363e28928b540ba7e7a49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 29 Apr 2016 13:40:05 -0300 Subject: [PATCH 815/890] v0.2.19 --- CHANGELOG.md | 21 +++++++++++++++++++-- social/__init__.py | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f65d2a8af..5da2db3fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,24 @@ # Change Log -## [v0.2.17](https://github.com/omab/python-social-auth/tree/v0.2.17) (2016-04-20) +## [v0.2.19](https://github.com/omab/python-social-auth/tree/v0.2.19) (2016-04-29) + +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.18...v0.2.19) + +**Closed issues:** + +- \[Flask\] Not Logged in After Redirect [\#913](https://github.com/omab/python-social-auth/issues/913) +- Django: type\(social\_user.extra\_data\) == unicode [\#898](https://github.com/omab/python-social-auth/issues/898) +- Email is empty in login with Facebook [\#889](https://github.com/omab/python-social-auth/issues/889) +**Merged pull requests:** + +- Updates to OpenIdConnectAuth [\#911](https://github.com/omab/python-social-auth/pull/911) ([clintonb](https://github.com/clintonb)) +- Corrected default value of JSONField [\#908](https://github.com/omab/python-social-auth/pull/908) ([clintonb](https://github.com/clintonb)) + +## [v0.2.18](https://github.com/omab/python-social-auth/tree/v0.2.18) (2016-04-20) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.17...v0.2.18) + +## [v0.2.17](https://github.com/omab/python-social-auth/tree/v0.2.17) (2016-04-20) [Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.16...v0.2.17) **Merged pull requests:** @@ -927,7 +944,7 @@ **Merged pull requests:** - Fix OpenId auth with Flask 0.10 [\#16](https://github.com/omab/python-social-auth/pull/16) ([Flyflo](https://github.com/Flyflo)) -- Add CodersClan button [\#13](https://github.com/omab/python-social-auth/pull/13) ([Orchestrator81](https://github.com/Orchestrator81)) +- Add CodersClan button [\#13](https://github.com/omab/python-social-auth/pull/13) ([DrorCohenCC](https://github.com/DrorCohenCC)) - Added a default to response in FacebookOAuth.do\_auth [\#12](https://github.com/omab/python-social-auth/pull/12) ([san-mate](https://github.com/san-mate)) - Bug fix of FacebookAppOAuth2 [\#11](https://github.com/omab/python-social-auth/pull/11) ([san-mate](https://github.com/san-mate)) diff --git a/social/__init__.py b/social/__init__.py index c13010fe4..895ff9af9 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 18) +version = (0, 2, 19) extra = '' __version__ = '.'.join(map(str, version)) + extra From 957578efb6b50156675747dc84f8adedf5f6aeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 5 May 2016 13:33:07 -0300 Subject: [PATCH 816/890] PEP8 --- social/backends/oauth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 6b3b6a3d6..3182e52b0 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -360,8 +360,11 @@ def auth_headers(self): def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token, token_type, and extra defined names to store in extra_data field""" - data = super(BaseOAuth2, self).extra_data(user, uid, response, details=details, *args, **kwargs) - data['token_type'] = response.get('token_type') or kwargs.get('token_type') + data = super(BaseOAuth2, self).extra_data(user, uid, response, + details=details, + *args, **kwargs) + data['token_type'] = response.get('token_type') or \ + kwargs.get('token_type') return data def request_access_token(self, *args, **kwargs): From 128759d773ed99be8221eb599add55f187a2f293 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 9 May 2016 20:10:21 +0100 Subject: [PATCH 817/890] Add Edmodo OAuth2 back-end --- docs/backends/edmodo.rst | 22 ++++++++++++++ docs/backends/index.rst | 1 + docs/thanks.rst | 2 ++ social/backends/edmodo.py | 34 +++++++++++++++++++++ social/tests/backends/test_edmodo.py | 44 ++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 docs/backends/edmodo.rst create mode 100644 social/backends/edmodo.py create mode 100644 social/tests/backends/test_edmodo.py diff --git a/docs/backends/edmodo.rst b/docs/backends/edmodo.rst new file mode 100644 index 000000000..095c45627 --- /dev/null +++ b/docs/backends/edmodo.rst @@ -0,0 +1,22 @@ +Edmodo +====== + +Edmodo supports OAuth 2. + +- Register a new application at `Edmodo Connect API`_, and follow the + instructions below. +- Add the Edmodo OAuth2 backend to your settings page:: + + SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + ... + 'social.backends.edmodo.EdmodoOAuth2', + ... + ) + +- Fill ``App Key``, ``App Secret`` and ``App Scope`` values in the settings:: + + SOCIAL_AUTH_EDMODO_OAUTH2_KEY = '' + SOCIAL_AUTH_EDMODO_OAUTH2_SECRET = '' + SOCIAL_AUTH_EDMODO_SCOPE = ['basic'] + +.. _Edmodo Connect API: https://developers.edmodo.com/edmodo-connect/edmodo-connect-overview-getting-started/ diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 7e3a23541..797bd6c41 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -71,6 +71,7 @@ Social backends dribbble drip dropbox + edmodo eveonline evernote facebook diff --git a/docs/thanks.rst b/docs/thanks.rst index ca4a40e68..46d7a2078 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -110,6 +110,7 @@ let me know and I'll update the list): * vbsteven_ * sbassi_ * aspcanada_ + * browniebroke_ .. _python-social-auth: https://github.com/omab/python-social-auth @@ -215,3 +216,4 @@ let me know and I'll update the list): .. _vbsteven: https://github.com/vbsteven .. _sbassi: https://github.com/sbassi .. _aspcanada: https://github.com/aspcanada +.. _browniebroke: https://github.com/browniebroke diff --git a/social/backends/edmodo.py b/social/backends/edmodo.py new file mode 100644 index 000000000..cc735897d --- /dev/null +++ b/social/backends/edmodo.py @@ -0,0 +1,34 @@ +""" +Edmodo OAuth2 Sign-in backend, docs at: + http://psa.matiasaguirre.net/docs/backends/edmodo.html +""" +from social.backends.oauth import BaseOAuth2 + + +class EdmodoOAuth2(BaseOAuth2): + """Edmodo OAuth2""" + name = 'edmodo' + AUTHORIZATION_URL = 'https://api.edmodo.com/oauth/authorize' + ACCESS_TOKEN_URL = 'https://api.edmodo.com/oauth/token' + ACCESS_TOKEN_METHOD = 'POST' + + def get_user_details(self, response): + """Return user details from Edmodo account""" + fullname, first_name, last_name = self.get_user_names( + first_name=response.get('first_name'), + last_name=response.get('last_name') + ) + return { + 'username': response.get('username'), + 'email': response.get('email'), + 'fullname': fullname, + 'first_name': first_name, + 'last_name': last_name + } + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from Edmodo""" + return self.get_json( + 'https://api.edmodo.com/users/me', + params={'access_token': access_token} + ) diff --git a/social/tests/backends/test_edmodo.py b/social/tests/backends/test_edmodo.py new file mode 100644 index 000000000..ff3914c1d --- /dev/null +++ b/social/tests/backends/test_edmodo.py @@ -0,0 +1,44 @@ +import json + +from social.tests.backends.oauth import OAuth2Test + + +class EdmodoOAuth2Test(OAuth2Test): + backend_path = 'social.backends.edmodo.EdmodoOAuth2' + user_data_url = 'https://api.edmodo.com/users/me' + expected_username = 'foobar12345' + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' + }) + user_data_body = json.dumps({ + 'username': 'foobar12345', + 'coppa_verified': False, + 'first_name': 'Foo', + 'last_name': 'Bar', + 'premium': False, + 'verified_institution_member': False, + 'url': 'https://api.edmodo.com/users/12345', + 'type': 'teacher', + 'time_zone': None, + 'end_level': None, + 'start_level': None, + 'locale': 'en', + 'subjects': None, + 'utc_offset': None, + 'email': 'foo.bar@example.com', + 'gender': None, + 'about': None, + 'user_title': None, + 'id': 12345, + 'avatars': { + 'small': 'https://api.edmodo.com/users/12345/avatar?type=small&u=5a15xug93m53mi4ey3ck4fvkq', + 'large': 'https://api.edmodo.com/users/12345/avatar?type=large&u=5a15xug93m53mi4ey3ck4fvkq' + } + }) + + def test_login(self): + self.do_login() + + def test_partial_pipeline(self): + self.do_partial_pipeline() From 494c400aa08991feeba5d738ffabc98b10dda324 Mon Sep 17 00:00:00 2001 From: andela-kerinoso Date: Tue, 10 May 2016 13:09:22 +0100 Subject: [PATCH 818/890] Fix Mixed-content error of loading http over https scheme after diconnect of social account --- social/actions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/social/actions.py b/social/actions.py index 4b005cf99..a0e4e5bf1 100644 --- a/social/actions.py +++ b/social/actions.py @@ -110,8 +110,10 @@ def do_disconnect(backend, user, association_id=None, redirect_name='next', if isinstance(response, dict): response = backend.strategy.redirect( - backend.strategy.request_data().get(redirect_name, '') or - backend.setting('DISCONNECT_REDIRECT_URL') or - backend.setting('LOGIN_REDIRECT_URL') + backend.strategy.absolute_uri( + backend.strategy.request_data().get(redirect_name, '') or + backend.setting('DISCONNECT_REDIRECT_URL') or + backend.setting('LOGIN_REDIRECT_URL') + ) ) return response From 8d60913f2350f0f7c6c7eb46c7d65dee0a6e8872 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Mon, 16 May 2016 13:45:32 +0600 Subject: [PATCH 819/890] Fixed typo --- docs/backends/facebook.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/facebook.rst b/docs/backends/facebook.rst index cf9136516..894070b3c 100644 --- a/docs/backends/facebook.rst +++ b/docs/backends/facebook.rst @@ -18,7 +18,7 @@ development resources`_: SOCIAL_AUTH_FACEBOOK_SECRET = '' - Define ``SOCIAL_AUTH_FACEBOOK_SCOPE`` to get extra permissions - from facebook. Email is not sent by deafault, to get it, you must request the + from facebook. Email is not sent by default, to get it, you must request the ``email`` permission:: SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] From 4273876cefa5b6d6e384c66188f6f64683990013 Mon Sep 17 00:00:00 2001 From: alexpantyukhin Date: Wed, 1 Jun 2016 19:54:45 +0500 Subject: [PATCH 820/890] add support peewee for flask --- examples/flask_peewee_example/__init__.py | 62 ++++++ examples/flask_peewee_example/manage.py | 26 +++ .../flask_peewee_example/models/__init__.py | 4 + examples/flask_peewee_example/models/user.py | 24 +++ .../flask_peewee_example/requirements.txt | 7 + .../flask_peewee_example/routes/__init__.py | 2 + examples/flask_peewee_example/routes/main.py | 22 ++ examples/flask_peewee_example/settings.py | 55 +++++ .../flask_peewee_example/templates/base.html | 14 ++ .../flask_peewee_example/templates/done.html | 24 +++ .../flask_peewee_example/templates/home.html | 85 ++++++++ social/apps/flask_app/peewee/__init__.py | 0 social/apps/flask_app/peewee/models.py | 48 +++++ social/storage/peewee_orm.py | 199 ++++++++++++++++++ 14 files changed, 572 insertions(+) create mode 100644 examples/flask_peewee_example/__init__.py create mode 100644 examples/flask_peewee_example/manage.py create mode 100644 examples/flask_peewee_example/models/__init__.py create mode 100644 examples/flask_peewee_example/models/user.py create mode 100644 examples/flask_peewee_example/requirements.txt create mode 100644 examples/flask_peewee_example/routes/__init__.py create mode 100644 examples/flask_peewee_example/routes/main.py create mode 100644 examples/flask_peewee_example/settings.py create mode 100644 examples/flask_peewee_example/templates/base.html create mode 100644 examples/flask_peewee_example/templates/done.html create mode 100644 examples/flask_peewee_example/templates/home.html create mode 100644 social/apps/flask_app/peewee/__init__.py create mode 100644 social/apps/flask_app/peewee/models.py create mode 100644 social/storage/peewee_orm.py diff --git a/examples/flask_peewee_example/__init__.py b/examples/flask_peewee_example/__init__.py new file mode 100644 index 000000000..4c2543f26 --- /dev/null +++ b/examples/flask_peewee_example/__init__.py @@ -0,0 +1,62 @@ +import sys + +from flask import Flask, g +from flask.ext import login + +sys.path.append('../..') + +from social.apps.flask_app.routes import social_auth +from social.apps.flask_app.template_filters import backends +from social.apps.flask_app.peewee.models import * +from peewee import * + +# App +app = Flask(__name__) +app.config.from_object('flask_example.settings') + +try: + app.config.from_object('flask_example.local_settings') +except ImportError: + pass + +from models.user import database_proxy, User + +# DB +database = SqliteDatabase('test.db') +database_proxy.initialize(database) + +app.register_blueprint(social_auth) +init_social(app, database) + +login_manager = login.LoginManager() +login_manager.login_view = 'main' +login_manager.login_message = '' +login_manager.init_app(app) + +from flask_example import models +from flask_example import routes + + +@login_manager.user_loader +def load_user(userid): + try: + us = User.get(User.id == userid) + return us + except User.DoesNotExist: + pass + + +@app.before_request +def global_user(): + g.user = login.current_user._get_current_object() + + +@app.context_processor +def inject_user(): + try: + return {'user': g.user} + except AttributeError: + return {'user': None} + + +app.context_processor(backends) diff --git a/examples/flask_peewee_example/manage.py b/examples/flask_peewee_example/manage.py new file mode 100644 index 000000000..927058674 --- /dev/null +++ b/examples/flask_peewee_example/manage.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +import sys + +from flask.ext.script import Server, Manager, Shell + +sys.path.append('..') + +from flask_example import app, database + + +manager = Manager(app) +manager.add_command('runserver', Server()) +manager.add_command('shell', Shell(make_context=lambda: { + 'app': app +})) + + +@manager.command +def syncdb(): + from flask_example.models.user import User + from social.apps.flask_app.peewee.models import FlaskStorage + + database.create_tables([User, FlaskStorage.user, FlaskStorage.nonce, FlaskStorage.association, FlaskStorage.code]) + +if __name__ == '__main__': + manager.run() diff --git a/examples/flask_peewee_example/models/__init__.py b/examples/flask_peewee_example/models/__init__.py new file mode 100644 index 000000000..2253824a3 --- /dev/null +++ b/examples/flask_peewee_example/models/__init__.py @@ -0,0 +1,4 @@ +from flask_example.models import user +from social.apps.flask_app.peewee import models +# create a peewee database instance -- our models will use this database to +# persist information diff --git a/examples/flask_peewee_example/models/user.py b/examples/flask_peewee_example/models/user.py new file mode 100644 index 000000000..a46f559cb --- /dev/null +++ b/examples/flask_peewee_example/models/user.py @@ -0,0 +1,24 @@ +from peewee import * +from datetime import datetime +from flask.ext.login import UserMixin + +database_proxy = Proxy() + + +# model definitions -- the standard "pattern" is to define a base model class +# that specifies which database to use. then, any subclasses will automatically +# use the correct storage. +class BaseModel(Model): + class Meta: + database = database_proxy + +# the user model specifies its fields (or columns) declaratively, like django +class User(BaseModel, UserMixin): + username = CharField(unique=True) + password = CharField(null=True) + email = CharField(null=True) + active = BooleanField(default=True) + join_date = DateTimeField(default=datetime.now) + + class Meta: + order_by = ('username',) diff --git a/examples/flask_peewee_example/requirements.txt b/examples/flask_peewee_example/requirements.txt new file mode 100644 index 000000000..e52656b9e --- /dev/null +++ b/examples/flask_peewee_example/requirements.txt @@ -0,0 +1,7 @@ +Peewee +Flask +Flask-Login +Flask-Script +Werkzeug +pysqlite +Jinja2 diff --git a/examples/flask_peewee_example/routes/__init__.py b/examples/flask_peewee_example/routes/__init__.py new file mode 100644 index 000000000..d3586e81b --- /dev/null +++ b/examples/flask_peewee_example/routes/__init__.py @@ -0,0 +1,2 @@ +from flask_example.routes import main +from social.apps.flask_app import routes diff --git a/examples/flask_peewee_example/routes/main.py b/examples/flask_peewee_example/routes/main.py new file mode 100644 index 000000000..5e0dd9cbc --- /dev/null +++ b/examples/flask_peewee_example/routes/main.py @@ -0,0 +1,22 @@ +from flask import render_template, redirect +from flask.ext.login import login_required, logout_user + +from flask_example import app + + +@app.route('/') +def main(): + return render_template('home.html') + + +@login_required +@app.route('/done/') +def done(): + return render_template('done.html') + + +@app.route('/logout') +def logout(): + """Logout view""" + logout_user() + return redirect('/') diff --git a/examples/flask_peewee_example/settings.py b/examples/flask_peewee_example/settings.py new file mode 100644 index 000000000..419c57518 --- /dev/null +++ b/examples/flask_peewee_example/settings.py @@ -0,0 +1,55 @@ +from os.path import dirname, abspath + +SECRET_KEY = 'random-secret-key' +SESSION_COOKIE_NAME = 'psa_session' +DEBUG = True +DEBUG_TB_INTERCEPT_REDIRECTS = False +SESSION_PROTECTION = 'strong' + +SOCIAL_AUTH_STORAGE = 'social.apps.flask_app.peewee.models.FlaskStorage' +SOCIAL_AUTH_LOGIN_URL = '/' +SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/' +SOCIAL_AUTH_USER_MODEL = 'flask_example.models.user.User' +SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + 'social.backends.open_id.OpenIdAuth', + 'social.backends.google.GoogleOpenId', + 'social.backends.google.GoogleOAuth2', + 'social.backends.google.GoogleOAuth', + 'social.backends.twitter.TwitterOAuth', + 'social.backends.yahoo.YahooOpenId', + 'social.backends.stripe.StripeOAuth2', + 'social.backends.persona.PersonaAuth', + 'social.backends.facebook.FacebookOAuth2', + 'social.backends.facebook.FacebookAppOAuth2', + 'social.backends.yahoo.YahooOAuth', + 'social.backends.angel.AngelOAuth2', + 'social.backends.behance.BehanceOAuth2', + 'social.backends.bitbucket.BitbucketOAuth', + 'social.backends.box.BoxOAuth2', + 'social.backends.linkedin.LinkedinOAuth', + 'social.backends.github.GithubOAuth2', + 'social.backends.foursquare.FoursquareOAuth2', + 'social.backends.instagram.InstagramOAuth2', + 'social.backends.live.LiveOAuth2', + 'social.backends.vk.VKOAuth2', + 'social.backends.dailymotion.DailymotionOAuth2', + 'social.backends.disqus.DisqusOAuth2', + 'social.backends.dropbox.DropboxOAuth', + 'social.backends.eveonline.EVEOnlineOAuth2', + 'social.backends.evernote.EvernoteSandboxOAuth', + 'social.backends.fitbit.FitbitOAuth2', + 'social.backends.flickr.FlickrOAuth', + 'social.backends.livejournal.LiveJournalOpenId', + 'social.backends.soundcloud.SoundcloudOAuth2', + 'social.backends.thisismyjam.ThisIsMyJamOAuth1', + 'social.backends.stocktwits.StocktwitsOAuth2', + 'social.backends.tripit.TripItOAuth', + 'social.backends.clef.ClefOAuth2', + 'social.backends.twilio.TwilioAuth', + 'social.backends.xing.XingOAuth', + 'social.backends.yandex.YandexOAuth2', + 'social.backends.podio.PodioOAuth2', + 'social.backends.reddit.RedditOAuth2', + 'social.backends.mineid.MineIDOAuth2', + 'social.backends.wunderlist.WunderlistOAuth2', +) diff --git a/examples/flask_peewee_example/templates/base.html b/examples/flask_peewee_example/templates/base.html new file mode 100644 index 000000000..86db50440 --- /dev/null +++ b/examples/flask_peewee_example/templates/base.html @@ -0,0 +1,14 @@ + + + + Social + + + + {% block content %}{% endblock %} + {% block scripts %}{% endblock %} + + + + + diff --git a/examples/flask_peewee_example/templates/done.html b/examples/flask_peewee_example/templates/done.html new file mode 100644 index 000000000..ccabf53ec --- /dev/null +++ b/examples/flask_peewee_example/templates/done.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

          You are logged in as {{ user.username }}!

          + +

          Associated:

          +{% for assoc in backends.associated %} +
          + {{ assoc.provider }} +
          + +
          +
          +{% endfor %} + +

          Associate:

          +
            + {% for name in backends.not_associated %} +
          • + {{ name }} +
          • + {% endfor %} +
          +{% endblock %} diff --git a/examples/flask_peewee_example/templates/home.html b/examples/flask_peewee_example/templates/home.html new file mode 100644 index 000000000..1c7f9bcef --- /dev/null +++ b/examples/flask_peewee_example/templates/home.html @@ -0,0 +1,85 @@ +{% extends "base.html" %} + +{% block content %} +Google OAuth2
          +Google OAuth
          +Google OpenId
          +Twitter OAuth
          +Yahoo OpenId
          +Yahoo OAuth
          +Stripe OAuth2
          +Facebook OAuth2
          +Facebook App
          +Angel OAuth2
          +Behance OAuth2
          +Bitbucket OAuth
          +Box OAuth2
          +LinkedIn OAuth
          +Github OAuth2
          +Foursquare OAuth2
          +Instagram OAuth2
          +Live OAuth2
          +VK.com OAuth2
          +Dailymotion OAuth2
          +Disqus OAuth2
          +Dropbox OAuth
          +Evernote OAuth (sandbox mode)
          +Fitbit OAuth
          +Flickr OAuth
          +Soundcloud OAuth2
          +ThisIsMyJam OAuth1
          +Stocktwits OAuth2
          +Tripit OAuth
          +Clef OAuth2
          +Twilio
          +Xing OAuth
          +Yandex OAuth2
          +Podio OAuth2
          +MineID OAuth2
          + +
          +
          + + + +
          +
          + +
          +
          + + + +
          +
          + +
          + + Persona +
          +{% endblock %} + +{% block scripts %} + + + +{% endblock %} diff --git a/social/apps/flask_app/peewee/__init__.py b/social/apps/flask_app/peewee/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py new file mode 100644 index 000000000..774ced6c4 --- /dev/null +++ b/social/apps/flask_app/peewee/models.py @@ -0,0 +1,48 @@ +"""Flask SQLAlchemy ORM models for Social Auth""" +from peewee import Model, ForeignKeyField, Proxy + +from social.utils import setting_name, module_member +from social.storage.peewee_orm import PeeweeUserMixin, \ + PeeweeAssociationMixin, \ + PeeweeNonceMixin, \ + PeeweeCodeMixin, \ + BasePeeweeStorage, \ + database_proxy + + +class FlaskStorage(BasePeeweeStorage): + user = None + nonce = None + association = None + code = None + + +def init_social(app, db): + User = module_member(app.config[setting_name('USER_MODEL')]) + + database_proxy.initialize(db) + + class UserSocialAuth(PeeweeUserMixin): + """Social Auth association model""" + user = ForeignKeyField(User, related_name='social_auth') + + @classmethod + def user_model(cls): + return User + + class Nonce(PeeweeNonceMixin): + """One use numbers""" + pass + + class Association(PeeweeAssociationMixin): + """OpenId account association""" + pass + + class Code(PeeweeCodeMixin): + pass + + # Set the references in the storage class + FlaskStorage.user = UserSocialAuth + FlaskStorage.nonce = Nonce + FlaskStorage.association = Association + FlaskStorage.code = Code diff --git a/social/storage/peewee_orm.py b/social/storage/peewee_orm.py new file mode 100644 index 000000000..66d0d49f4 --- /dev/null +++ b/social/storage/peewee_orm.py @@ -0,0 +1,199 @@ +import six +import base64 + +from peewee import CharField, Model, Proxy, IntegrityError +from playhouse.kv import JSONField + +from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ + CodeMixin, BaseStorage + + +def get_query_by_dict_param(cls, params): + q = True + for field_name, value in params.iteritems(): + query_item = cls._meta.fields[field_name] == value + + q = q & query_item + + return q + + +database_proxy = Proxy() + + +class BaseModel(Model): + class Meta: + database = database_proxy + + +class PeeweeUserMixin(UserMixin, BaseModel): + provider = CharField() + extra_data = JSONField(null=True) + uid = CharField() + user = None + + @classmethod + def changed(cls, user): + user.save() + + def set_extra_data(self, extra_data=None): + if super(PeeweeUserMixin, self).set_extra_data(extra_data): + self.save() + + @classmethod + def username_max_length(cls): + username_field = cls.username_field() + field = getattr(cls.user_model(), username_field) + return field.max_length + + @classmethod + def username_field(cls): + return getattr(cls.user_model(), 'USERNAME_FIELD', 'username') + + @classmethod + def allowed_to_disconnect(cls, user, backend_name, association_id=None): + if association_id is not None: + qs = cls.select().where(cls.id != association_id) + else: + qs = cls.select().where(cls.provider != backend_name) + qs = qs.where(cls.user == user) + + if hasattr(user, 'has_usable_password'): + valid_password = user.has_usable_password() + else: + valid_password = True + return valid_password or qs.count() > 0 + + @classmethod + def disconnect(cls, entry): + entry.delete_instance() + + @classmethod + def user_exists(cls, *args, **kwargs): + """ + Return True/False if a User instance exists with the given arguments. + """ + user_model = cls.user_model() + + q = get_query_by_dict_param(user_model, kwargs) + + return user_model.select().where(q).count() > 0 + + @classmethod + def get_username(cls, user): + return getattr(user, cls.username_field(), None) + + @classmethod + def create_user(cls, *args, **kwargs): + username_field = cls.username_field() + if 'username' in kwargs and username_field not in kwargs: + kwargs[username_field] = kwargs.pop('username') + return cls.user_model().create(*args, **kwargs) + + @classmethod + def get_user(cls, pk, **kwargs): + if pk: + kwargs = {'id': pk} + try: + return cls.user_model().select().get(get_query_by_dict_param(cls.user_model(), kwargs)) + except cls.user_model().DoesNotExist: + return None + + @classmethod + def get_users_by_email(cls, email): + user_model = cls.user_model() + return user_model.select().where(user_model.email == email) + + @classmethod + def get_social_auth(cls, provider, uid): + if not isinstance(uid, six.string_types): + uid = str(uid) + try: + return cls.select().where(cls.provider == provider, cls.uid == uid).get() + except cls.DoesNotExist: + return None + + @classmethod + def get_social_auth_for_user(cls, user, provider=None, id=None): + qs = cls.select().where(cls.user == user) + if provider: + qs = qs.where(cls.provider == provider) + if id: + qs = qs.where(cls.id == id) + return list(qs) + + @classmethod + def create_social_auth(cls, user, uid, provider): + if not isinstance(uid, six.string_types): + uid = str(uid) + return cls.create(user=user, uid=uid, provider=provider) + + +class PeeweeNonceMixin(NonceMixin, BaseModel): + server_url = CharField() + timestamp = CharField() + salt = CharField() + + @classmethod + def use(cls, server_url, timestamp, salt): + return cls.select().get_or_create(cls.server_url == server_url, + cls.timestamp == timestamp, + cls.salt == salt) + + +class PeeweeAssociationMixin(AssociationMixin, BaseModel): + server_url = CharField() + handle = CharField() + secret = CharField() # base64 encoded + issued = CharField() + lifetime = CharField() + assoc_type = CharField() + + + @classmethod + def store(cls, server_url, association): + try: + assoc = cls.select().get(cls.server_url == server_url, + cls.handle == association.handle) + except cls.DoesNotExist: + assoc = cls(server_url=server_url, + handle=association.handle) + + assoc.secret = base64.encodestring(association.secret) + assoc.issued = association.issued + assoc.lifetime = association.lifetime + assoc.assoc_type = association.assoc_type + assoc.save() + + @classmethod + def get(cls, *args, **kwargs): + q = get_query_by_dict_param(cls, kwargs) + return cls.select().where(q) + + @classmethod + def remove(cls, ids_to_delete): + cls.select().where(cls.id << ids_to_delete).delete() + + +class PeeweeCodeMixin(CodeMixin, BaseModel): + email = CharField() + code = CharField() # base64 encoded + issued = CharField() + + @classmethod + def get_code(cls, code): + try: + return cls.select().get(cls.code == code) + except cls.DoesNotExist: + return None + + +class BasePeeweeStorage(BaseStorage): + user = PeeweeUserMixin + nonce = PeeweeNonceMixin + association = PeeweeAssociationMixin + code = PeeweeCodeMixin + + @classmethod + def is_integrity_error(cls, exception): + return exception.__class__ is IntegrityError From bc43e2572f5d94f305b494892d7a68dcc2b4d365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Jun 2016 21:58:31 -0300 Subject: [PATCH 821/890] PEP8 --- docs/backends/sketchfab.rst | 8 +++++--- social/backends/sketchfab.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/backends/sketchfab.rst b/docs/backends/sketchfab.rst index 865b8c483..c69a388e2 100644 --- a/docs/backends/sketchfab.rst +++ b/docs/backends/sketchfab.rst @@ -5,11 +5,13 @@ Sketchfab uses OAuth 2 for authentication. To use: -- Follow the steps at `Sketchfab Oauth`_, and ask for an ``Authorization code`` grant type. +- Follow the steps at `Sketchfab Oauth`_, and ask for an + ``Authorization code`` grant type. -- Fill the ``Client id/key`` and ``Client Secret`` values you received in your django settings:: +- Fill the ``Client id/key`` and ``Client Secret`` values you received + in your django settings:: SOCIAL_AUTH_SKETCHFAB_KEY = '' SOCIAL_AUTH_SKETCHFAB_SECRET = '' -.. _Sketchfab Oauth: https://sketchfab.com/developers/oauth \ No newline at end of file +.. _Sketchfab Oauth: https://sketchfab.com/developers/oauth diff --git a/social/backends/sketchfab.py b/social/backends/sketchfab.py index 76140413c..cb19ef5d4 100644 --- a/social/backends/sketchfab.py +++ b/social/backends/sketchfab.py @@ -34,5 +34,6 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - return self.get_json('https://sketchfab.com/v2/users/me', - headers={'Authorization': 'Bearer {0}'.format(access_token)}) + return self.get_json('https://sketchfab.com/v2/users/me', headers={ + 'Authorization': 'Bearer {0}'.format(access_token) + }) From a52e3d374eed220fcc846f2221ae27e09d452c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Jun 2016 22:07:19 -0300 Subject: [PATCH 822/890] PEP8 --- social/backends/weixin.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/social/backends/weixin.py b/social/backends/weixin.py index 0eb81e5e6..279a03085 100644 --- a/social/backends/weixin.py +++ b/social/backends/weixin.py @@ -44,7 +44,9 @@ def user_data(self, access_token, *args, **kwargs): nickname = data.get('nickname') if nickname: # weixin api has some encode bug, here need handle - data['nickname'] = nickname.encode('raw_unicode_escape').decode('utf-8') + data['nickname'] = nickname.encode( + 'raw_unicode_escape' + ).decode('utf-8') return data def auth_params(self, state=None): @@ -103,9 +105,10 @@ def auth_complete(self, *args, **kwargs): class WeixinOAuth2APP(WeixinOAuth2): - """Weixin OAuth authentication backend + """ + Weixin OAuth authentication backend - can't use in web, only in weixin app + Can't use in web, only in weixin app """ name = 'weixinapp' ID_KEY = 'openid' @@ -133,17 +136,18 @@ def auth_url(self): params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) params = urllib.urlencode(sorted(params.items())) - return '{}#wechat_redirect'.format(self.AUTHORIZATION_URL + '?' + params) - + return '{}#wechat_redirect'.format( + self.AUTHORIZATION_URL + '?' + params + ) def auth_complete_params(self, state=None): - appid, secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'appid': appid, - 'secret': secret, - } + appid, secret = self.get_key_and_secret() + return { + 'grant_type': 'authorization_code', # request auth code + 'code': self.data.get('code', ''), # server response code + 'appid': appid, + 'secret': secret, + } def validate_state(self): return None From 9d8073f419178d2ec61b98c79931570a4b8a9e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Jun 2016 22:10:33 -0300 Subject: [PATCH 823/890] PEP8 --- docs/backends/untappd.rst | 3 ++- social/backends/untappd.py | 26 ++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/backends/untappd.rst b/docs/backends/untappd.rst index bb1157a91..0b41a1273 100644 --- a/docs/backends/untappd.rst +++ b/docs/backends/untappd.rst @@ -5,7 +5,8 @@ Untappd uses OAuth v2 for Authentication, check the `official docs`_. - Create an app by filling out the form here: `Add App`_ -- Apps are approved on a one-by-one basis, so you'll need to wait a few days to get your client ID and secret. +- Apps are approved on a one-by-one basis, so you'll need to wait a + few days to get your client ID and secret. - Fill ``Client ID`` and ``Client Secret`` values in the settings:: diff --git a/social/backends/untappd.py b/social/backends/untappd.py index 8d5b71df6..7d241adb5 100644 --- a/social/backends/untappd.py +++ b/social/backends/untappd.py @@ -36,7 +36,9 @@ def auth_params(self, state=None): return params def process_error(self, data): - """ All errors from Untappd are contained in the 'meta' key of the response. """ + """ + All errors from Untappd are contained in the 'meta' key of the response. + """ response_code = data.get('meta', {}).get('http_code') if response_code is not None and response_code != requests.codes.ok: raise AuthFailed(self, data['meta']['error_detail']) @@ -49,7 +51,8 @@ def auth_complete(self, *args, **kwargs): self.process_error(self.data) - # Untapped sends the access token request with URL parameters, not a body + # Untapped sends the access token request with URL parameters, + # not a body response = self.request_access_token( self.access_token_url(), method=self.ACCESS_TOKEN_METHOD, @@ -64,9 +67,13 @@ def auth_complete(self, *args, **kwargs): self.process_error(response) - # Both the access_token and the rest of the response are buried in the 'response' key - return self.do_auth(response['response']['access_token'], response=response['response'], - *args, **kwargs) + # Both the access_token and the rest of the response are + # buried in the 'response' key + return self.do_auth( + response['response']['access_token'], + response=response['response'], + *args, **kwargs + ) def get_user_details(self, response): """Return user details from an Untappd account""" @@ -79,13 +86,16 @@ def get_user_details(self, response): 'email': user_data.get('settings', {}).get('email_address', ''), 'first_name': user_data.get('first_name'), 'last_name': user_data.get('last_name'), - 'fullname': user_data.get('first_name') + ' ' + user_data.get('last_name') + 'fullname': user_data.get('first_name') + ' ' + + user_data.get('last_name') }) return user_data def get_user_id(self, details, response): - """Return a unique ID for the current user, by default from server - response.""" + """ + Return a unique ID for the current user, by default from + server response. + """ return response['user'].get(self.ID_KEY) def user_data(self, access_token, *args, **kwargs): From 93eb2a1b78166695a00ad56021182ffa4f33e859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Jun 2016 22:12:14 -0300 Subject: [PATCH 824/890] PEP8 --- social/backends/coding.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/social/backends/coding.py b/social/backends/coding.py index ee9b2afa3..3b4295eae 100644 --- a/social/backends/coding.py +++ b/social/backends/coding.py @@ -41,5 +41,8 @@ def user_data(self, access_token, *args, **kwargs): return data.get('data') def _user_data(self, access_token, path=None): - url = urljoin(self.api_url(), 'account/current_user{0}'.format(path or '')) + url = urljoin( + self.api_url(), + 'account/current_user{0}'.format(path or '') + ) return self.get_json(url, params={'access_token': access_token}) From c9d77ae0997052f3abdb63cc7f3353575e944fbc Mon Sep 17 00:00:00 2001 From: Sergey Date: Sun, 5 Jun 2016 19:11:30 +0300 Subject: [PATCH 825/890] adds custom get_user_id to coursera backend --- social/backends/coursera.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/social/backends/coursera.py b/social/backends/coursera.py index 9507edbbd..ed32721ee 100644 --- a/social/backends/coursera.py +++ b/social/backends/coursera.py @@ -28,6 +28,10 @@ def get_user_details(self, response): """Return user details from Coursera account""" return {'username': self._get_username_from_response(response)} + def get_user_id(self, details, response): + """Return a username prepared in get_user_details as uid""" + return details.get(self.ID_KEY) + def user_data(self, access_token, *args, **kwargs): """Load user data from the service""" return self.get_json( From ee16b369eeacce5aa40103479f0fb4d3206e561c Mon Sep 17 00:00:00 2001 From: Philip Garnero Date: Sun, 5 Jun 2016 19:24:23 -0700 Subject: [PATCH 826/890] fix first and last name recovery --- social/backends/uber.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/social/backends/uber.py b/social/backends/uber.py index ef6c3815a..d83f9b05d 100644 --- a/social/backends/uber.py +++ b/social/backends/uber.py @@ -19,7 +19,8 @@ def auth_complete_credentials(self): def get_user_details(self, response): """Return user details from Uber account""" email = response.get('email', '') - fullname, first_name, last_name = self.get_user_names() + fullname, first_name, last_name = self.get_user_names('', response.get('first_name', ''), + response.get('last_name', '')) return {'username': email, 'email': email, 'fullname': fullname, From d9b7b5f029f31bf6c037839483d099fd5ef34660 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Fri, 17 Jun 2016 11:26:00 +0700 Subject: [PATCH 827/890] django migration should respect SOCIAL_AUTH_USER_MODEL setting --- .../default/migrations/0002_add_related_name.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py index 8e39f15bf..848a64af1 100644 --- a/social/apps/django_app/default/migrations/0002_add_related_name.py +++ b/social/apps/django_app/default/migrations/0002_add_related_name.py @@ -4,6 +4,12 @@ from django.db import models, migrations from django.conf import settings +from social.utils import setting_name + +USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ + getattr(settings, 'AUTH_USER_MODEL', None) or \ + 'auth.User' + class Migration(migrations.Migration): @@ -15,6 +21,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='usersocialauth', name='user', - field=models.ForeignKey(related_name='social_auth', to=settings.AUTH_USER_MODEL) + field=models.ForeignKey(related_name='social_auth', to=USER_MODEL) ), ] From 61cd24ef8aa394df96554c187d49cc251cb9f8aa Mon Sep 17 00:00:00 2001 From: Goncharov Ilia Date: Sun, 19 Jun 2016 20:10:53 +0300 Subject: [PATCH 828/890] Line support added --- README.rst | 2 + docs/backends/line.rst | 7 ++++ social/backends/line.py | 87 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 docs/backends/line.rst create mode 100644 social/backends/line.py diff --git a/README.rst b/README.rst index bcc9a6b5a..1b1500262 100644 --- a/README.rst +++ b/README.rst @@ -84,6 +84,7 @@ or current ones extended): * Kakao_ OAuth2 https://developer.kakao.com * `Khan Academy`_ OAuth1 * Launchpad_ OpenId + * Line_ OAuth2 * Linkedin_ OAuth1 * Live_ OAuth2 * Livejournal_ OpenId @@ -265,6 +266,7 @@ check `django-social-auth LICENSE`_ for details: .. _Instagram: https://instagram.com .. _Itembase: https://www.itembase.com .. _LaunchPad: https://help.launchpad.net/YourAccount/OpenID +.. _Line: https://line.me/ .. _Linkedin: https://www.linkedin.com .. _Live: https://live.com .. _Livejournal: http://livejournal.com diff --git a/docs/backends/line.rst b/docs/backends/line.rst new file mode 100644 index 000000000..93fc479d6 --- /dev/null +++ b/docs/backends/line.rst @@ -0,0 +1,7 @@ +Line.me +========== + +Fill App Id and Secret in your project settings:: + + SOCIAL_AUTH_LINE_KEY = '...' + SOCIAL_AUTH_LINE_SECRET = '...' diff --git a/social/backends/line.py b/social/backends/line.py new file mode 100644 index 000000000..347d52e1b --- /dev/null +++ b/social/backends/line.py @@ -0,0 +1,87 @@ +# vim:fileencoding=utf-8 +import requests +import json + +from social.backends.oauth import BaseOAuth2 +from social.exceptions import AuthFailed +from social.utils import handle_http_errors + + +class LineOAuth2(BaseOAuth2): + name = 'line' + AUTHORIZATION_URL = 'https://access.line.me/dialog/oauth/weblogin' + ACCESS_TOKEN_URL = 'https://api.line.me/v1/oauth/accessToken' + BASE_API_URL = 'https://api.line.me' + USER_INFO_URL = BASE_API_URL + '/v1/profile' + ACCESS_TOKEN_METHOD = 'POST' + STATE_PARAMETER = True + REDIRECT_STATE = True + ID_KEY = 'mid' + EXTRA_DATA = [ + ('mid', 'id'), + ('expire', 'expire'), + ('refreshToken', 'refresh_token') + ] + + def auth_params(self, state=None): + client_id, client_secret = self.get_key_and_secret() + params = { + 'client_id': client_id, + 'redirect_uri': self.get_redirect_uri(), + 'response_type': self.RESPONSE_TYPE + } + return params + + def process_error(self, data): + error_code = data.get('errorCode') or data.get('statusCode') or data.get('error') + error_message = data.get('errorMessage') or data.get('statusMessage') or data.get('error_desciption') + if error_code is not None or error_message is not None: + raise AuthFailed(self, error_message or error_code) + + @handle_http_errors + def auth_complete(self, *args, **kwargs): + """Completes login process, must return user instance""" + client_id, client_secret = self.get_key_and_secret() + code = self.data.get('code') + + self.process_error(self.data) + + try: + response = self.request_access_token( + self.access_token_url(), + method=self.ACCESS_TOKEN_METHOD, + params={ + 'requestToken': code, + 'channelSecret': client_secret + } + ) + self.process_error(response) + + return self.do_auth(response['accessToken'], response=response, *args, **kwargs) + except requests.HTTPError as err: + self.process_error(json.loads(err.response.content)) + + def get_user_details(self, response): + response.update({ + 'fullname': response.get('displayName'), + 'picture_url': response.get('pictureUrl') + }) + return response + + def get_user_id(self, details, response): + """Return a unique ID for the current user, by default from server response.""" + return response.get(self.ID_KEY) + + def user_data(self, access_token, *args, **kwargs): + """Loads user data from service""" + try: + response = self.get_json( + self.USER_INFO_URL, + headers={ + "Authorization": "Bearer {}".format(access_token) + } + ) + self.process_error(response) + return response + except requests.HTTPError as err: + self.process_error(err.response.json()) From ed633c471d9eff70a65a4f9096badc16474ae50f Mon Sep 17 00:00:00 2001 From: Phivos Stylianides Date: Tue, 28 Jun 2016 16:36:49 +0200 Subject: [PATCH 829/890] Upgrade facebook backend api to latest version (v2.6) --- social/backends/facebook.py | 8 ++++---- social/tests/backends/test_facebook.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index f76b502c9..59ee09fb9 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -19,11 +19,11 @@ class FacebookOAuth2(BaseOAuth2): name = 'facebook' RESPONSE_TYPE = None SCOPE_SEPARATOR = ',' - AUTHORIZATION_URL = 'https://www.facebook.com/v2.3/dialog/oauth' - ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.3/oauth/access_token' - REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.3/{uid}/permissions' + AUTHORIZATION_URL = 'https://www.facebook.com/v2.6/dialog/oauth' + ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.6/oauth/access_token' + REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.6/{uid}/permissions' REVOKE_TOKEN_METHOD = 'DELETE' - USER_DATA_URL = 'https://graph.facebook.com/v2.3/me' + USER_DATA_URL = 'https://graph.facebook.com/v2.6/me' EXTRA_DATA = [ ('id', 'id'), ('expires', 'expires') diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py index 166d75327..2a460d72a 100644 --- a/social/tests/backends/test_facebook.py +++ b/social/tests/backends/test_facebook.py @@ -7,7 +7,7 @@ class FacebookOAuth2Test(OAuth2Test): backend_path = 'social.backends.facebook.FacebookOAuth2' - user_data_url = 'https://graph.facebook.com/v2.3/me' + user_data_url = 'https://graph.facebook.com/v2.6/me' expected_username = 'foobar' access_token_body = json.dumps({ 'access_token': 'foobar', From 45e68ddab12abb72bd78b7b3891db0d5f1146d8d Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 13 Jul 2016 05:49:51 +0100 Subject: [PATCH 830/890] Switch from flask.ext.login to flask_login Importing flask.ext.login is deprecated, use flask_login instead. --- examples/flask_example/models/user.py | 2 +- examples/flask_example/routes/main.py | 2 +- examples/flask_me_example/models/user.py | 2 +- examples/flask_me_example/routes/main.py | 2 +- social/apps/flask_app/routes.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/flask_example/models/user.py b/examples/flask_example/models/user.py index 7cb70580e..08bcee8da 100644 --- a/examples/flask_example/models/user.py +++ b/examples/flask_example/models/user.py @@ -1,7 +1,7 @@ from sqlalchemy import Column, String, Integer, Boolean from sqlalchemy.ext.declarative import declarative_base -from flask.ext.login import UserMixin +from flask_login import UserMixin from flask_example import db_session diff --git a/examples/flask_example/routes/main.py b/examples/flask_example/routes/main.py index 5e0dd9cbc..fd6526185 100644 --- a/examples/flask_example/routes/main.py +++ b/examples/flask_example/routes/main.py @@ -1,5 +1,5 @@ from flask import render_template, redirect -from flask.ext.login import login_required, logout_user +from flask_login import login_required, logout_user from flask_example import app diff --git a/examples/flask_me_example/models/user.py b/examples/flask_me_example/models/user.py index 035b5276b..d351fed77 100644 --- a/examples/flask_me_example/models/user.py +++ b/examples/flask_me_example/models/user.py @@ -1,6 +1,6 @@ from mongoengine import StringField, EmailField, BooleanField -from flask.ext.login import UserMixin +from flask_login import UserMixin from flask_me_example import db diff --git a/examples/flask_me_example/routes/main.py b/examples/flask_me_example/routes/main.py index 127986ef0..d2cfd82d5 100644 --- a/examples/flask_me_example/routes/main.py +++ b/examples/flask_me_example/routes/main.py @@ -1,5 +1,5 @@ from flask import render_template, redirect -from flask.ext.login import login_required, logout_user +from flask_login import login_required, logout_user from flask_me_example import app diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index 6c1eec79d..b3b140631 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -1,5 +1,5 @@ from flask import g, Blueprint, request -from flask.ext.login import login_required, login_user +from flask_login import login_required, login_user from social.actions import do_auth, do_complete, do_disconnect from social.apps.flask_app.utils import psa From f1716dc0155a7b0f352acf54cf6ecf4a8642c8bf Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 13 Jul 2016 12:00:21 +0100 Subject: [PATCH 831/890] username max_length can be None When using postgres it is possible to make the username field a varchar with no maximum size. --- social/pipeline/user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social/pipeline/user.py b/social/pipeline/user.py index a46fc3e63..e5f8d65fd 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -39,7 +39,9 @@ def get_username(strategy, details, user=None, *args, **kwargs): else: username = uuid4().hex - short_username = username[:max_length - uuid_length] + short_username = (username[:max_length - uuid_length] + if max_length is not None + else username) final_username = slug_func(clean_func(username[:max_length])) # Generate a unique username for current user using username From 8f74d33eb7390a6638ac766976f2245732503186 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 13 Jul 2016 14:16:48 +0100 Subject: [PATCH 832/890] More switching from flask.ext.login to flask_login --- examples/flask_example/__init__.py | 6 +++--- examples/flask_me_example/__init__.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py index d24fb82e4..cbfa7c606 100644 --- a/examples/flask_example/__init__.py +++ b/examples/flask_example/__init__.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker from flask import Flask, g -from flask.ext import login +from flask_login import LoginManager, current_user sys.path.append('../..') @@ -29,7 +29,7 @@ app.register_blueprint(social_auth) init_social(app, db_session) -login_manager = login.LoginManager() +login_manager = LoginManager() login_manager.login_view = 'main' login_manager.login_message = '' login_manager.init_app(app) @@ -48,7 +48,7 @@ def load_user(userid): @app.before_request def global_user(): - g.user = login.current_user + g.user = current_user @app.teardown_appcontext diff --git a/examples/flask_me_example/__init__.py b/examples/flask_me_example/__init__.py index f838a206c..9f5aaacb3 100644 --- a/examples/flask_me_example/__init__.py +++ b/examples/flask_me_example/__init__.py @@ -1,7 +1,7 @@ import sys from flask import Flask, g -from flask.ext import login +from flask_login import LoginManager, current_user from flask.ext.mongoengine import MongoEngine sys.path.append('../..') @@ -26,7 +26,7 @@ app.register_blueprint(social_auth) init_social(app, db) -login_manager = login.LoginManager() +login_manager = LoginManager() login_manager.login_view = 'main' login_manager.login_message = '' login_manager.init_app(app) @@ -45,7 +45,7 @@ def load_user(userid): @app.before_request def global_user(): - g.user = login.current_user + g.user = current_user @app.context_processor From ef5394f048304cd139da4da03e43bc1d554d309f Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 14 Jul 2016 11:30:38 +0700 Subject: [PATCH 833/890] Add atomic transaction around social_auth creation Cherry-pick of https://github.com/omab/python-social-auth/pull/770 --- social/storage/django_orm.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 8d9e67246..637dd23a2 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -1,6 +1,7 @@ """Django ORM models for Social Auth""" import base64 import six +from django.db import transaction from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage @@ -96,7 +97,16 @@ def get_social_auth_for_user(cls, user, provider=None, id=None): def create_social_auth(cls, user, uid, provider): if not isinstance(uid, six.string_types): uid = str(uid) - return cls.objects.create(user=user, uid=uid, provider=provider) + if hasattr(transaction, 'atomic'): + # In Django versions that have an "atomic" transaction decorator / context + # manager, there's a transaction wrapped around this call. + # If the create fails below due to an IntegrityError, ensure that the transaction + # stays undamaged by wrapping the create in an atomic. + with transaction.atomic(): + social_auth = cls.objects.create(user=user, uid=uid, provider=provider) + else: + social_auth = cls.objects.create(user=user, uid=uid, provider=provider) + return social_auth class DjangoNonceMixin(NonceMixin): From 4e2d69b4643f1ce09cca880a007f16c6027b299e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 17 Jul 2016 03:52:46 -0300 Subject: [PATCH 834/890] PEP8 --- docs/backends/line.rst | 2 +- social/backends/line.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/backends/line.rst b/docs/backends/line.rst index 93fc479d6..f63c611a1 100644 --- a/docs/backends/line.rst +++ b/docs/backends/line.rst @@ -1,5 +1,5 @@ Line.me -========== +======= Fill App Id and Secret in your project settings:: diff --git a/social/backends/line.py b/social/backends/line.py index 347d52e1b..ba8136ed8 100644 --- a/social/backends/line.py +++ b/social/backends/line.py @@ -25,16 +25,19 @@ class LineOAuth2(BaseOAuth2): def auth_params(self, state=None): client_id, client_secret = self.get_key_and_secret() - params = { + return { 'client_id': client_id, 'redirect_uri': self.get_redirect_uri(), 'response_type': self.RESPONSE_TYPE } - return params def process_error(self, data): - error_code = data.get('errorCode') or data.get('statusCode') or data.get('error') - error_message = data.get('errorMessage') or data.get('statusMessage') or data.get('error_desciption') + error_code = data.get('errorCode') or \ + data.get('statusCode') or \ + data.get('error') + error_message = data.get('errorMessage') or \ + data.get('statusMessage') or \ + data.get('error_desciption') if error_code is not None or error_message is not None: raise AuthFailed(self, error_message or error_code) @@ -57,7 +60,8 @@ def auth_complete(self, *args, **kwargs): ) self.process_error(response) - return self.do_auth(response['accessToken'], response=response, *args, **kwargs) + return self.do_auth(response['accessToken'], response=response, + *args, **kwargs) except requests.HTTPError as err: self.process_error(json.loads(err.response.content)) From c0ea7c4b15fcd64b7b3bbd44d409a29771f6ea05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 17 Jul 2016 04:14:40 -0300 Subject: [PATCH 835/890] Remove swap file --- social/backends/.reddit.py.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 social/backends/.reddit.py.swp diff --git a/social/backends/.reddit.py.swp b/social/backends/.reddit.py.swp deleted file mode 100644 index ee5262440b855e77069f633858a24096e3f82a76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2O>f*p7{{j^5O1Lo2nlf-!ePAW#+$SuphyvN6Ng5myOGyhRaBKTYtLrgcx^K? z4un<82S9=g7Y-aa!Ub{Vg!&oa#GBy8cPNNw#@>xLtw^q*vGk8UGtcwPJimESqD+7J z*3M1*hP{g5nMdfe&T}tWN@y;#{2aUcI$$e5mcuMv5?L72 zWQi7X*ebtUeHtFJG!iY&q9~Sjetb?u(`*PB0v94s$idQ^bLiUDm1SlB+QO^&#TR-P zay3SVfFWQA7y^cXAz%m?0)~Jg@c$qn^GoO>Nacwto!6`Pr8DoUXkLbZAz%m?0)~Jg zU@LHnQp z^cH9y^zeCvz5^YBdZ25dmqCBeA@mFA0q7gh=b$mD2Wo)+dJdtVKtF*X^c zluMB_Zh89VW5YyhzzW1~jV#M-hs)B8p{ zOIZ^yQhp%dwRrfx`f9Ah?@85jGW}8&Wj|Guev$F=$@+NZgxG_OkEpa3b;<`!28Zl8 zplK8c76LV^(Xd$>W)ZVyqY#y2#^VpEjI)%i<371gMI2&+ui!ovY;Dypcp~V41^uu5F`Y8W(Msf9!LCl2=QaWM)oscFb39VPm=Bm618__tKSPzOM zIrTL<$*kX<4$LC#gB-S}2x6EKNTw{2(=}ni2Ci)%Wi(q+8?;4Wo<}T&bxvX7 zE8E$M8QUaI4^Itir5P&?1_DjV&9ugm>I8mPweD@-bAm0$ci_~xqtooV?X95Kb-loO z+im+$@AHD`rY(2d>FxSKceAtSDhG%JqmSh=DG}+0pzH2Ap5u2sFdP1ad{5wqeU?b^-%?flfI^| z7G(%qN8tLQZNVYNN Date: Sun, 17 Jul 2016 20:11:00 +0200 Subject: [PATCH 836/890] Fix for flask/sqlachemy: commit on save (but not when using Pyramid) --- social/apps/pyramid_app/models.py | 2 ++ social/storage/sqlalchemy_orm.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py index 2d70e0681..5752f3f51 100644 --- a/social/apps/pyramid_app/models.py +++ b/social/apps/pyramid_app/models.py @@ -24,6 +24,8 @@ def init_social(config, Base, session): app_session = session class _AppSession(object): + COMMIT_SESSION = False + @classmethod def _session(cls): return app_session diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index 621bf4c1f..e48ef64cf 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -28,6 +28,8 @@ def __init__(self, *args, **kwargs): class SQLAlchemyMixin(object): + COMMIT_SESSION = True + @classmethod def _session(cls): raise NotImplementedError('Implement in subclass') @@ -43,7 +45,11 @@ def _new_instance(cls, model, *args, **kwargs): @classmethod def _save_instance(cls, instance): cls._session().add(instance) - cls._flush() + if cls.COMMIT_SESSION: + cls._session().commit() + cls._session().flush() + else: + cls._flush() return instance @classmethod From db8598f94b1e58719e4a77823d26480192dbd7c5 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 25 Jul 2016 14:16:52 -0400 Subject: [PATCH 837/890] Add redirect_uri to yammer docs --- docs/backends/yammer.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/backends/yammer.rst b/docs/backends/yammer.rst index df6104dbf..86f4c74c3 100644 --- a/docs/backends/yammer.rst +++ b/docs/backends/yammer.rst @@ -10,7 +10,8 @@ Production Mode In order to enable the backend, follow: -- Register an application at `Client Applications`_ +- Register an application at `Client Applications`_, + set the ``Redirect URI`` to ``http:///complete/yammer/`` - Fill **Client Key** and **Client Secret** settings:: From 8aca599083f69fbc4427f608554d4035c7d2dccf Mon Sep 17 00:00:00 2001 From: murchik Date: Mon, 25 Jul 2016 04:29:18 +0800 Subject: [PATCH 838/890] Allow POST requests for auth method so OpenID forms could use it that way. --- social/apps/pyramid_app/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/pyramid_app/views.py b/social/apps/pyramid_app/views.py index 38ee4fa33..7587ff929 100644 --- a/social/apps/pyramid_app/views.py +++ b/social/apps/pyramid_app/views.py @@ -5,7 +5,7 @@ from social.apps.pyramid_app.utils import psa, login_required -@view_config(route_name='social.auth', request_method='GET') +@view_config(route_name='social.auth', request_method=('GET', 'POST')) @psa('social.complete') def auth(request): return do_auth(request.backend, redirect_name='next') From a7c15f32cc3e9d5514700eae7680955c95414e73 Mon Sep 17 00:00:00 2001 From: murchik Date: Mon, 25 Jul 2016 04:40:14 +0800 Subject: [PATCH 839/890] "else" scenario causes an exception every time. Not sure if it is a right solution, but it fixes everything. --- social/strategies/pyramid_strategy.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/social/strategies/pyramid_strategy.py b/social/strategies/pyramid_strategy.py index 6ac41573c..9761a42b7 100644 --- a/social/strategies/pyramid_strategy.py +++ b/social/strategies/pyramid_strategy.py @@ -38,13 +38,7 @@ def get_setting(self, name): def html(self, content): """Return HTTP response with given content""" - response = getattr(self.request, 'response', None) - if response is None: - response = Response(body=content) - else: - response = self.request.response - response.body = content - return response + return Response(body=content) def request_data(self, merge=True): """Return current request data (POST or GET)""" From ed274822027cac1c5be3cd98a129dba1a93c8d20 Mon Sep 17 00:00:00 2001 From: murchik Date: Tue, 26 Jul 2016 23:41:21 +0800 Subject: [PATCH 840/890] Multiple hosts in redirect sanitaion. --- social/actions.py | 11 +++++++---- social/tests/test_utils.py | 24 +++++++++++++++++------- social/utils.py | 10 +++++----- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/social/actions.py b/social/actions.py index a0e4e5bf1..09732197a 100644 --- a/social/actions.py +++ b/social/actions.py @@ -19,8 +19,9 @@ def do_auth(backend, redirect_name='next'): # Check and sanitize a user-defined GET/POST next field value redirect_uri = data[redirect_name] if backend.setting('SANITIZE_REDIRECTS', True): - redirect_uri = sanitize_redirect(backend.strategy.request_host(), - redirect_uri) + allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + [ + backend.strategy.request_host()] + redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri) backend.strategy.session_set( redirect_name, redirect_uri or backend.setting('LOGIN_REDIRECT_URL') @@ -91,8 +92,10 @@ def do_complete(backend, login, user=None, redirect_name='next', '{0}={1}'.format(redirect_name, redirect_value) if backend.setting('SANITIZE_REDIRECTS', True): - url = sanitize_redirect(backend.strategy.request_host(), url) or \ - backend.setting('LOGIN_REDIRECT_URL') + allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + [ + backend.strategy.request_host()] + url = sanitize_redirect(allowed_hosts, url) or \ + backend.setting('LOGIN_REDIRECT_URL') return backend.strategy.redirect(url) diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py index 7bd8db089..8ab2eadb5 100644 --- a/social/tests/test_utils.py +++ b/social/tests/test_utils.py @@ -13,31 +13,41 @@ class SanitizeRedirectTest(unittest.TestCase): def test_none_redirect(self): - self.assertEqual(sanitize_redirect('myapp.com', None), None) + self.assertEqual(sanitize_redirect(['myapp.com'], None), None) def test_empty_redirect(self): - self.assertEqual(sanitize_redirect('myapp.com', ''), None) + self.assertEqual(sanitize_redirect(['myapp.com'], ''), None) def test_dict_redirect(self): - self.assertEqual(sanitize_redirect('myapp.com', {}), None) + self.assertEqual(sanitize_redirect(['myapp.com'], {}), None) def test_invalid_redirect(self): - self.assertEqual(sanitize_redirect('myapp.com', {'foo': 'bar'}), None) + self.assertEqual(sanitize_redirect(['myapp.com'], {'foo': 'bar'}), None) def test_wrong_path_redirect(self): self.assertEqual( - sanitize_redirect('myapp.com', 'http://notmyapp.com/path/'), + sanitize_redirect(['myapp.com'], 'http://notmyapp.com/path/'), None ) def test_valid_absolute_redirect(self): self.assertEqual( - sanitize_redirect('myapp.com', 'http://myapp.com/path/'), + sanitize_redirect(['myapp.com'], 'http://myapp.com/path/'), 'http://myapp.com/path/' ) def test_valid_relative_redirect(self): - self.assertEqual(sanitize_redirect('myapp.com', '/path/'), '/path/') + self.assertEqual(sanitize_redirect(['myapp.com'], '/path/'), '/path/') + + def test_multiple_hosts(self): + allowed_hosts = ['myapp1.com', 'myapp2.com'] + for host in allowed_hosts: + url = 'http://{}/path/'.format(host) + self.assertEqual(sanitize_redirect(allowed_hosts, url), url) + + def test_multiple_hosts_wrong_host(self): + self.assertEqual(sanitize_redirect( + ['myapp1.com', 'myapp2.com'], 'http://notmyapp.com/path/'), None) class UserIsAuthenticatedTest(unittest.TestCase): diff --git a/social/utils.py b/social/utils.py index 0b5a50759..c70db5231 100644 --- a/social/utils.py +++ b/social/utils.py @@ -81,21 +81,21 @@ def setting_name(*names): return to_setting_name(*((SETTING_PREFIX,) + names)) -def sanitize_redirect(host, redirect_to): +def sanitize_redirect(hosts, redirect_to): """ - Given the hostname and an untrusted URL to redirect to, + Given a list of hostnames and an untrusted URL to redirect to, this method tests it to make sure it isn't garbage/harmful and returns it, else returns None, similar as how's it done on django.contrib.auth.views. """ if redirect_to: try: - # Don't redirect to a different host - netloc = urlparse(redirect_to)[1] or host + # Don't redirect to a host not in a list + netloc = urlparse(redirect_to)[1] or hosts[0] except (TypeError, AttributeError): pass else: - if netloc == host: + if netloc in hosts: return redirect_to From 65b68d5e47f6990625c19afe8317397bdbbb11cd Mon Sep 17 00:00:00 2001 From: Zuhaib M Siddique Date: Tue, 26 Jul 2016 16:37:04 -0700 Subject: [PATCH 841/890] Removed dep method get_all_field_name method from Django 1.8+ --- social/apps/django_app/default/admin.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index de7802bf9..cf17b7733 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -24,11 +24,23 @@ def get_search_fields(self, request=None): hasattr(_User, 'username') and 'username' or \ None fieldnames = ('first_name', 'last_name', 'email', username) - all_names = _User._meta.get_all_field_names() + all_names = self._get_all_field_names(_User._meta) search_fields = [name for name in fieldnames if name and name in all_names] return ['user__' + name for name in search_fields] + @staticmethod + def _get_all_field_names(model): + from itertools import chain + + return list(set(chain.from_iterable( + (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) + for field in model.get_fields() + # For complete backwards compatibility, you may want to exclude + # GenericForeignKey from the results. + if not (field.many_to_one and field.related_model is None) + ))) + class NonceOption(admin.ModelAdmin): """Nonce options""" From 46c2cc35df83ed494670a0655fe6c80162a78b87 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Wed, 27 Jul 2016 22:38:40 -0400 Subject: [PATCH 842/890] Corrected migration dependency This appears to have been broken since it was introduced by #908. --- .../django_app/default/migrations/0004_auto_20160423_0400.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py index 668bf0e3d..3a1c0b089 100644 --- a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py +++ b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('default', '0003_alter_email_max_length'), + ('social_auth', '0003_alter_email_max_length'), ] operations = [ From ac42c84a71568ba8289118ece49675f05e1a1ffa Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Wed, 27 Jul 2016 22:44:17 -0400 Subject: [PATCH 843/890] Added index to Django Association model This unique_together index on the server_url and handle columns ensures lookups do not result in full table scans. Fixes #967 --- .../migrations/0005_auto_20160727_2333.py | 19 +++++++++++++++++++ social/apps/django_app/default/models.py | 3 +++ 2 files changed, 22 insertions(+) create mode 100644 social/apps/django_app/default/migrations/0005_auto_20160727_2333.py diff --git a/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py b/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py new file mode 100644 index 000000000..3df56ef1c --- /dev/null +++ b/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-07-28 02:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('social_auth', '0004_auto_20160423_0400'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='association', + unique_together=set([('server_url', 'handle')]), + ), + ] diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index 9f18529bb..047da9177 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -96,6 +96,9 @@ class Association(models.Model, DjangoAssociationMixin): class Meta: db_table = 'social_auth_association' + unique_together = ( + ('server_url', 'handle',) + ) class Code(models.Model, DjangoCodeMixin): From 97eea03e6b70eecddacbdbe97d9ace248148bd88 Mon Sep 17 00:00:00 2001 From: c-bata Date: Mon, 1 Aug 2016 15:20:45 +0900 Subject: [PATCH 844/890] Update facebook api version to v2.7 --- social/backends/facebook.py | 8 ++++---- social/tests/backends/test_facebook.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 59ee09fb9..f904556c9 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -19,11 +19,11 @@ class FacebookOAuth2(BaseOAuth2): name = 'facebook' RESPONSE_TYPE = None SCOPE_SEPARATOR = ',' - AUTHORIZATION_URL = 'https://www.facebook.com/v2.6/dialog/oauth' - ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.6/oauth/access_token' - REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.6/{uid}/permissions' + AUTHORIZATION_URL = 'https://www.facebook.com/v2.7/dialog/oauth' + ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.7/oauth/access_token' + REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.7/{uid}/permissions' REVOKE_TOKEN_METHOD = 'DELETE' - USER_DATA_URL = 'https://graph.facebook.com/v2.6/me' + USER_DATA_URL = 'https://graph.facebook.com/v2.7/me' EXTRA_DATA = [ ('id', 'id'), ('expires', 'expires') diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py index 2a460d72a..81146d962 100644 --- a/social/tests/backends/test_facebook.py +++ b/social/tests/backends/test_facebook.py @@ -7,7 +7,7 @@ class FacebookOAuth2Test(OAuth2Test): backend_path = 'social.backends.facebook.FacebookOAuth2' - user_data_url = 'https://graph.facebook.com/v2.6/me' + user_data_url = 'https://graph.facebook.com/v2.7/me' expected_username = 'foobar' access_token_body = json.dumps({ 'access_token': 'foobar', From 9f86059e9d8070bc5ecd7ba069fadab1c9bf502a Mon Sep 17 00:00:00 2001 From: Carson Gee Date: Mon, 1 Aug 2016 17:49:20 -0400 Subject: [PATCH 845/890] Added exception handling for user creation race condition in Django --- social/storage/django_orm.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index 637dd23a2..e331656c8 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -1,7 +1,9 @@ """Django ORM models for Social Auth""" import base64 import six +import sys from django.db import transaction +from django.db.utils import IntegrityError from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ CodeMixin, BaseStorage @@ -58,7 +60,20 @@ def create_user(cls, *args, **kwargs): username_field = cls.username_field() if 'username' in kwargs and username_field not in kwargs: kwargs[username_field] = kwargs.pop('username') - return cls.user_model().objects.create_user(*args, **kwargs) + try: + user = cls.user_model().objects.create_user(*args, **kwargs) + except IntegrityError: + # User might have been created on a different thread, try and find them. + # If we don't, re-raise the IntegrityError. + exc_info = sys.exc_info() + # If email comes in as None it won't get found in the get + if kwargs.get('email', True) is None: + kwargs['email'] = '' + try: + user = cls.user_model().objects.get(*args, **kwargs) + except cls.user_model().DoesNotExist: + six.reraise(*exc_info) + return user @classmethod def get_user(cls, pk=None, **kwargs): From 74d8345c2a0ab93afb5d46a0fbf3d676bd04fbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 03:58:52 -0300 Subject: [PATCH 846/890] Move import to the top, apply PEP8 --- social/apps/django_app/default/admin.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index cf17b7733..5cee753fd 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -1,4 +1,6 @@ """Admin settings""" +from itertools import chain + from django.conf import settings from django.contrib import admin @@ -31,15 +33,15 @@ def get_search_fields(self, request=None): @staticmethod def _get_all_field_names(model): - from itertools import chain - - return list(set(chain.from_iterable( - (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) + names = chain.from_iterable( + (field.name, field.attname) + if hasattr(field, 'attname') else (field.name,) for field in model.get_fields() # For complete backwards compatibility, you may want to exclude # GenericForeignKey from the results. if not (field.many_to_one and field.related_model is None) - ))) + ) + return list(set(names)) class NonceOption(admin.ModelAdmin): From 3cb8bd557fd94f3d261471044506f2e2a69bceec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 04:22:36 -0300 Subject: [PATCH 847/890] Code style and PEP8 --- social/actions.py | 10 +++++----- social/utils.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/social/actions.py b/social/actions.py index 09732197a..af61bd733 100644 --- a/social/actions.py +++ b/social/actions.py @@ -19,8 +19,8 @@ def do_auth(backend, redirect_name='next'): # Check and sanitize a user-defined GET/POST next field value redirect_uri = data[redirect_name] if backend.setting('SANITIZE_REDIRECTS', True): - allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + [ - backend.strategy.request_host()] + allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \ + [backend.strategy.request_host()] redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri) backend.strategy.session_set( redirect_name, @@ -92,10 +92,10 @@ def do_complete(backend, login, user=None, redirect_name='next', '{0}={1}'.format(redirect_name, redirect_value) if backend.setting('SANITIZE_REDIRECTS', True): - allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + [ - backend.strategy.request_host()] + allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \ + [backend.strategy.request_host()] url = sanitize_redirect(allowed_hosts, url) or \ - backend.setting('LOGIN_REDIRECT_URL') + backend.setting('LOGIN_REDIRECT_URL') return backend.strategy.redirect(url) diff --git a/social/utils.py b/social/utils.py index c70db5231..e82af6912 100644 --- a/social/utils.py +++ b/social/utils.py @@ -90,7 +90,7 @@ def sanitize_redirect(hosts, redirect_to): """ if redirect_to: try: - # Don't redirect to a host not in a list + # Don't redirect to a host that's not in the list netloc = urlparse(redirect_to)[1] or hosts[0] except (TypeError, AttributeError): pass From b25de586550259976a2921332f4c7d99c6d0a1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 04:32:35 -0300 Subject: [PATCH 848/890] PEP8 --- social/backends/uber.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/social/backends/uber.py b/social/backends/uber.py index d83f9b05d..6b1463b94 100644 --- a/social/backends/uber.py +++ b/social/backends/uber.py @@ -19,8 +19,11 @@ def auth_complete_credentials(self): def get_user_details(self, response): """Return user details from Uber account""" email = response.get('email', '') - fullname, first_name, last_name = self.get_user_names('', response.get('first_name', ''), - response.get('last_name', '')) + fullname, first_name, last_name = self.get_user_names( + '', + response.get('first_name', ''), + response.get('last_name', '') + ) return {'username': email, 'email': email, 'fullname': fullname, @@ -29,12 +32,8 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - client_id, client_secret = self.get_key_and_secret() response = kwargs.pop('response') - return self.get_json('https://api.uber.com/v1/me', headers={ - 'Authorization': '{0} {1}'.format( - response.get('token_type'), access_token - ) - } - ) + 'Authorization': '{0} {1}'.format(response.get('token_type'), + access_token) + }) From fb194f7f26d65a1f2f8c4398c1c60d68d0246991 Mon Sep 17 00:00:00 2001 From: alexpantyukhin Date: Tue, 12 Apr 2016 18:48:54 +0500 Subject: [PATCH 849/890] fix the flask example. #895 --- examples/flask_example/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) mode change 100644 => 100755 examples/flask_example/__init__.py diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py old mode 100644 new mode 100755 index cbfa7c606..07c108083 --- a/examples/flask_example/__init__.py +++ b/examples/flask_example/__init__.py @@ -48,7 +48,8 @@ def load_user(userid): @app.before_request def global_user(): - g.user = current_user + # evaluate proxy value + g.user = current_user._get_current_object() @app.teardown_appcontext From e88774b11ad4816431bb3cd96bb9d9eb2cd196c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 05:24:15 -0300 Subject: [PATCH 850/890] Fix upwork test --- docs/backends/upwork.rst | 2 +- social/backends/upwork.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/backends/upwork.rst b/docs/backends/upwork.rst index 8e5d2c820..59b599096 100644 --- a/docs/backends/upwork.rst +++ b/docs/backends/upwork.rst @@ -1,5 +1,5 @@ Upwork -======= +====== Upwork supports only OAuth 1. diff --git a/social/backends/upwork.py b/social/backends/upwork.py index f7dcdc249..64f4debc1 100644 --- a/social/backends/upwork.py +++ b/social/backends/upwork.py @@ -17,11 +17,15 @@ class UpworkOAuth(BaseOAuth1): def get_user_details(self, response): """Return user details from Upwork account""" + info = response.get('info', {}) auth_user = response.get('auth_user', {}) first_name = auth_user.get('first_name') last_name = auth_user.get('last_name') fullname = '{} {}'.format(first_name, last_name) + profile_url = info.get('profile_url', '') + username = profile_url.rsplit('/')[-1].replace('~', '') return { + 'username': username, 'fullname': fullname, 'first_name': first_name, 'last_name': last_name From 06804a5c2b17c1e65db8c1ceef762a0e744bacf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 05:24:50 -0300 Subject: [PATCH 851/890] Link doc for line backend --- docs/backends/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 9943c70d6..37c29bbec 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -90,6 +90,7 @@ Social backends khanacademy lastfm launchpad + line linkedin livejournal live From e0ee2607a7446be919cd27396441c490f4eca608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 05:32:22 -0300 Subject: [PATCH 852/890] Simplify new_association check --- social/pipeline/social_auth.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index ff99404a0..b181ba556 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -17,7 +17,6 @@ def auth_allowed(backend, details, response, *args, **kwargs): def social_user(backend, uid, user=None, *args, **kwargs): provider = backend.name - new_association = True social = backend.strategy.storage.user.get_social_auth(provider, uid) if social: if user and social.user != user: @@ -25,11 +24,10 @@ def social_user(backend, uid, user=None, *args, **kwargs): raise AuthAlreadyAssociated(backend, msg) elif not user: user = social.user - new_association = False return {'social': social, 'user': user, 'is_new': user is None, - 'new_association': new_association} + 'new_association': social is None} def associate_user(backend, uid, user=None, social=None, *args, **kwargs): From 919b81b297ed1b00c0bd127affb7c47fb692a14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 6 Aug 2016 05:41:27 -0300 Subject: [PATCH 853/890] Update docs --- docs/backends/twitter.rst | 8 ++++---- social/tests/backends/test_twitter.py | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/backends/twitter.rst b/docs/backends/twitter.rst index 6fcedd811..64e885c1a 100644 --- a/docs/backends/twitter.rst +++ b/docs/backends/twitter.rst @@ -20,9 +20,9 @@ To enable Twitter these two keys are needed. Further documentation at Client type instead of the Browser. Almost any dummy value will work if you plan some test. -- You can request user's Email address by setting (consult `Twitter verify credentials`_):: - - SOCIAL_AUTH_TWITTER_INCLUDE_EMAIL = True +- You can request user's Email address (consult `Twitter verify + credentials`_), the parameter is sent automatically, but the + applicaton needs to be whitelisted in order to get a valid value. Twitter usually fails with a 401 error when trying to call the request-token URL, this is usually caused by server datetime errors (check miscellaneous @@ -31,4 +31,4 @@ the trick. .. _Twitter development resources: http://dev.twitter.com/pages/auth .. _Twitter App Creation: http://twitter.com/apps/new -.. _Twitter verify credentials: https://dev.twitter.com/rest/reference/get/account/verify_credentials \ No newline at end of file +.. _Twitter verify credentials: https://dev.twitter.com/rest/reference/get/account/verify_credentials diff --git a/social/tests/backends/test_twitter.py b/social/tests/backends/test_twitter.py index d4eaef8e4..fc2af1404 100644 --- a/social/tests/backends/test_twitter.py +++ b/social/tests/backends/test_twitter.py @@ -253,9 +253,6 @@ class TwitterOAuth1IncludeEmailTest(OAuth1Test): }) def test_login(self): - self.strategy.set_settings({ - 'SOCIAL_AUTH_TWITTER_INCLUDE_EMAIL': True - }) user = self.do_login() self.assertEquals(user.email, 'foo@bar.bas') From acc22c396879da2b1f964784d617cbdba5e97223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 7 Aug 2016 11:24:59 -0300 Subject: [PATCH 854/890] PEP8 --- social/apps/flask_app/peewee/models.py | 10 +++--- social/storage/peewee_orm.py | 46 +++++++++++++------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py index 774ced6c4..3d0a8500a 100644 --- a/social/apps/flask_app/peewee/models.py +++ b/social/apps/flask_app/peewee/models.py @@ -3,11 +3,11 @@ from social.utils import setting_name, module_member from social.storage.peewee_orm import PeeweeUserMixin, \ - PeeweeAssociationMixin, \ - PeeweeNonceMixin, \ - PeeweeCodeMixin, \ - BasePeeweeStorage, \ - database_proxy + PeeweeAssociationMixin, \ + PeeweeNonceMixin, \ + PeeweeCodeMixin, \ + BasePeeweeStorage, \ + database_proxy class FlaskStorage(BasePeeweeStorage): diff --git a/social/storage/peewee_orm.py b/social/storage/peewee_orm.py index 66d0d49f4..60b5da0f2 100644 --- a/social/storage/peewee_orm.py +++ b/social/storage/peewee_orm.py @@ -5,17 +5,16 @@ from playhouse.kv import JSONField from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ - CodeMixin, BaseStorage + CodeMixin, BaseStorage def get_query_by_dict_param(cls, params): - q = True + query = True + for field_name, value in params.iteritems(): query_item = cls._meta.fields[field_name] == value - - q = q & query_item - - return q + query = query & query_item + return query database_proxy = Proxy() @@ -53,16 +52,16 @@ def username_field(cls): @classmethod def allowed_to_disconnect(cls, user, backend_name, association_id=None): if association_id is not None: - qs = cls.select().where(cls.id != association_id) + query = cls.select().where(cls.id != association_id) else: - qs = cls.select().where(cls.provider != backend_name) - qs = qs.where(cls.user == user) + query = cls.select().where(cls.provider != backend_name) + query = query.where(cls.user == user) if hasattr(user, 'has_usable_password'): valid_password = user.has_usable_password() else: valid_password = True - return valid_password or qs.count() > 0 + return valid_password or query.count() > 0 @classmethod def disconnect(cls, entry): @@ -74,10 +73,8 @@ def user_exists(cls, *args, **kwargs): Return True/False if a User instance exists with the given arguments. """ user_model = cls.user_model() - - q = get_query_by_dict_param(user_model, kwargs) - - return user_model.select().where(q).count() > 0 + query = get_query_by_dict_param(user_model, kwargs) + return user_model.select().where(query).count() > 0 @classmethod def get_username(cls, user): @@ -95,7 +92,9 @@ def get_user(cls, pk, **kwargs): if pk: kwargs = {'id': pk} try: - return cls.user_model().select().get(get_query_by_dict_param(cls.user_model(), kwargs)) + return cls.user_model().select().get( + get_query_by_dict_param(cls.user_model(), kwargs) + ) except cls.user_model().DoesNotExist: return None @@ -109,18 +108,20 @@ def get_social_auth(cls, provider, uid): if not isinstance(uid, six.string_types): uid = str(uid) try: - return cls.select().where(cls.provider == provider, cls.uid == uid).get() + return cls.select().where( + cls.provider == provider, cls.uid == uid + ).get() except cls.DoesNotExist: return None @classmethod def get_social_auth_for_user(cls, user, provider=None, id=None): - qs = cls.select().where(cls.user == user) + query = cls.select().where(cls.user == user) if provider: - qs = qs.where(cls.provider == provider) + query = query.where(cls.provider == provider) if id: - qs = qs.where(cls.id == id) - return list(qs) + query = query.where(cls.id == id) + return list(query) @classmethod def create_social_auth(cls, user, uid, provider): @@ -149,7 +150,6 @@ class PeeweeAssociationMixin(AssociationMixin, BaseModel): lifetime = CharField() assoc_type = CharField() - @classmethod def store(cls, server_url, association): try: @@ -167,8 +167,8 @@ def store(cls, server_url, association): @classmethod def get(cls, *args, **kwargs): - q = get_query_by_dict_param(cls, kwargs) - return cls.select().where(q) + query = get_query_by_dict_param(cls, kwargs) + return cls.select().where(query) @classmethod def remove(cls, ids_to_delete): From c3fc520397943a7f66b8b1607b5db8044d14b2b8 Mon Sep 17 00:00:00 2001 From: alexpantyukhin Date: Sun, 7 Aug 2016 19:17:51 +0400 Subject: [PATCH 855/890] fix comment word --- social/apps/flask_app/peewee/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py index 3d0a8500a..497e9a5a4 100644 --- a/social/apps/flask_app/peewee/models.py +++ b/social/apps/flask_app/peewee/models.py @@ -1,4 +1,4 @@ -"""Flask SQLAlchemy ORM models for Social Auth""" +"""Flask Peewee ORM models for Social Auth""" from peewee import Model, ForeignKeyField, Proxy from social.utils import setting_name, module_member From 03ae00154900a8150a5bf941663425698aacdb6b Mon Sep 17 00:00:00 2001 From: seroy Date: Wed, 10 Aug 2016 12:24:41 +0300 Subject: [PATCH 856/890] Fixed Django < 1.8 broken compatibility This commit 10a6f5570a4471d156c2bc06ecd4313189d23172 broke compatible with Django < 1.8, fixed --- social/apps/django_app/default/fields.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py index 859338643..ab47fba91 100644 --- a/social/apps/django_app/default/fields.py +++ b/social/apps/django_app/default/fields.py @@ -1,5 +1,6 @@ import json import six +import functools from django.core.exceptions import ValidationError from django.db import models @@ -10,8 +11,14 @@ except ImportError: from django.utils.encoding import smart_text +try: + from django.db.models import SubfieldBase + field_class = functools.partial(six.with_metaclass, SubfieldBase) +except ImportError: + field_class = functools.partial(six.with_metaclass, type) + -class JSONField(models.TextField): +class JSONField(field_class(models.TextField)): """Simple JSON field that stores python structures as JSON strings on database. """ From 1310fdb26ca8894c362911c0198cea89fc34b789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Aug 2016 20:32:42 -0300 Subject: [PATCH 857/890] Remove broken badge --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 1b1500262..f2b051689 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,6 @@ for more frameworks and ORMs. .. image:: https://badge.fury.io/py/python-social-auth.png :target: http://badge.fury.io/py/python-social-auth -.. image:: https://pypip.in/d/python-social-auth/badge.png - :target: https://crate.io/packages/python-social-auth?version=latest - .. image:: https://readthedocs.org/projects/python-social-auth/badge/?version=latest :target: https://readthedocs.org/projects/python-social-auth/?badge=latest :alt: Documentation Status From 7d42b05e83494e44e7d6aa0ce250f0d61db9d8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Aug 2016 20:39:41 -0300 Subject: [PATCH 858/890] Remove leading space in authorization header --- social/backends/dribbble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py index e1c3eeeed..ab6d0e5ae 100644 --- a/social/backends/dribbble.py +++ b/social/backends/dribbble.py @@ -58,5 +58,5 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://api.dribbble.com/v1/user', headers={ - 'Authorization': ' Bearer {0}'.format(access_token) + 'Authorization': 'Bearer {0}'.format(access_token) }) From 0c9c12c68352bfaf01a815d7e6fec194401f4e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Aug 2016 20:50:06 -0300 Subject: [PATCH 859/890] Remove leading space in authorization header in qiita backend --- social/backends/qiita.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/qiita.py b/social/backends/qiita.py index 52b947a1a..92bcbd38e 100644 --- a/social/backends/qiita.py +++ b/social/backends/qiita.py @@ -62,5 +62,5 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://qiita.com/api/v2/authenticated_user', headers={ - 'Authorization': ' Bearer {0}'.format(access_token) + 'Authorization': 'Bearer {0}'.format(access_token) }) From 8638b75220175a56a4f84d3644ffcf80bde808d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 11 Aug 2016 21:23:25 -0300 Subject: [PATCH 860/890] v0.2.20 --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++-- social/__init__.py | 2 +- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da2db3fb..6effa10ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,54 @@ # Change Log -## [v0.2.19](https://github.com/omab/python-social-auth/tree/v0.2.19) (2016-04-29) +## [v0.2.20](https://github.com/omab/python-social-auth/tree/v0.2.20) (2016-08-11) + +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.19...HEAD) + +**Closed issues:** + +- On production /complete/facebook just times out with a Gateway Timeout [\#972](https://github.com/omab/python-social-auth/issues/972) +- Support namespace via : [\#971](https://github.com/omab/python-social-auth/issues/971) +- Django Association model missing index [\#967](https://github.com/omab/python-social-auth/issues/967) +- VK auth using access token failed. Unable to retrieve email address. [\#943](https://github.com/omab/python-social-auth/issues/943) +- ImportError: No module named django\_app [\#935](https://github.com/omab/python-social-auth/issues/935) +- ImportError: No module named 'example.local\_settings' with pyramid\_example [\#919](https://github.com/omab/python-social-auth/issues/919) +- "'User' object is not callable." issue. [\#895](https://github.com/omab/python-social-auth/issues/895) +- Support for the peewee ORM in storage. [\#877](https://github.com/omab/python-social-auth/issues/877) +- Meetup.com OAuth2 [\#677](https://github.com/omab/python-social-auth/issues/677) +**Merged pull requests:** + +- fix comment word [\#983](https://github.com/omab/python-social-auth/pull/983) ([alexpantyukhin](https://github.com/alexpantyukhin)) +- Added exception handling for user creation race condition in Django [\#975](https://github.com/omab/python-social-auth/pull/975) ([carsongee](https://github.com/carsongee)) +- Update facebook api version to v2.7 [\#973](https://github.com/omab/python-social-auth/pull/973) ([c-bata](https://github.com/c-bata)) +- Added index to Django Association model [\#969](https://github.com/omab/python-social-auth/pull/969) ([clintonb](https://github.com/clintonb)) +- Corrected migration dependency [\#968](https://github.com/omab/python-social-auth/pull/968) ([clintonb](https://github.com/clintonb)) +- Removed dep method get\_all\_field\_names method from Django 1.8+ [\#966](https://github.com/omab/python-social-auth/pull/966) ([zsiddique](https://github.com/zsiddique)) +- Multiple hosts in redirect sanitaion. [\#965](https://github.com/omab/python-social-auth/pull/965) ([moorchegue](https://github.com/moorchegue)) +- "else" scenario in Pyramid html func was causing an exception every time. [\#964](https://github.com/omab/python-social-auth/pull/964) ([moorchegue](https://github.com/moorchegue)) +- Allow POST requests for auth method so OpenID forms could use it that way [\#963](https://github.com/omab/python-social-auth/pull/963) ([moorchegue](https://github.com/moorchegue)) +- Add redirect\_uri to yammer docs [\#960](https://github.com/omab/python-social-auth/pull/960) ([m3brown](https://github.com/m3brown)) +- Fix for flask/SQLAlchemy: commit on save \(but not when using Pyramid\) [\#957](https://github.com/omab/python-social-auth/pull/957) ([aoghina](https://github.com/aoghina)) +- Switch from flask.ext.login to flask\_login [\#951](https://github.com/omab/python-social-auth/pull/951) ([EdwardBetts](https://github.com/EdwardBetts)) +- username max\_length can be None [\#950](https://github.com/omab/python-social-auth/pull/950) ([EdwardBetts](https://github.com/EdwardBetts)) +- Upgrade facebook backend api to latest version \(v2.6\) [\#941](https://github.com/omab/python-social-auth/pull/941) ([stphivos](https://github.com/stphivos)) +- Line support added [\#937](https://github.com/omab/python-social-auth/pull/937) ([polyn0m](https://github.com/polyn0m)) +- django migration should respect SOCIAL\_AUTH\_USER\_MODEL setting [\#936](https://github.com/omab/python-social-auth/pull/936) ([max-arnold](https://github.com/max-arnold)) +- fix first and last name recovery [\#934](https://github.com/omab/python-social-auth/pull/934) ([PhilipGarnero](https://github.com/PhilipGarnero)) +- fixes empty uid in coursera backend [\#933](https://github.com/omab/python-social-auth/pull/933) ([CrowbarKZ](https://github.com/CrowbarKZ)) +- add support peewee for flask \#877 [\#932](https://github.com/omab/python-social-auth/pull/932) ([alexpantyukhin](https://github.com/alexpantyukhin)) +- Fixed typo [\#928](https://github.com/omab/python-social-auth/pull/928) ([arogachev](https://github.com/arogachev)) +- Fix mixed-content error of loading http over https scheme after disconnection from social account [\#924](https://github.com/omab/python-social-auth/pull/924) ([andela-kerinoso](https://github.com/andela-kerinoso)) +- Add back-end for Edmodo [\#921](https://github.com/omab/python-social-auth/pull/921) ([browniebroke](https://github.com/browniebroke)) +- Add Django AppConfig Label of "social\_auth" for migrations [\#916](https://github.com/omab/python-social-auth/pull/916) ([cclay](https://github.com/cclay)) +- Update vk.rst [\#907](https://github.com/omab/python-social-auth/pull/907) ([slushkovsky](https://github.com/slushkovsky)) +- VULNERABILITY - BaseStrategy.validate\_email\(\) doesn't actually check email address [\#900](https://github.com/omab/python-social-auth/pull/900) ([scottp-dpaw](https://github.com/scottp-dpaw)) +- Removed broken link in use cases docs fixing \#860 [\#886](https://github.com/omab/python-social-auth/pull/886) ([RobinStephenson](https://github.com/RobinStephenson)) +- Fixes bug where partial pipelines from abandoned login attempts will be resumed … [\#882](https://github.com/omab/python-social-auth/pull/882) ([SeanHayes](https://github.com/SeanHayes)) +- Revise battlenet endpoint to return account ID and battletag [\#799](https://github.com/omab/python-social-auth/pull/799) ([ckcollab](https://github.com/ckcollab)) +- Fixed 401 client redirect error for reddit backend [\#772](https://github.com/omab/python-social-auth/pull/772) ([opaqe](https://github.com/opaqe)) + +## [v0.2.19](https://github.com/omab/python-social-auth/tree/v0.2.19) (2016-04-29) [Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.18...v0.2.19) **Closed issues:** @@ -12,6 +59,7 @@ **Merged pull requests:** +- Storing token\_type in extra\_data field when using OAuth 2.0 [\#912](https://github.com/omab/python-social-auth/pull/912) ([clintonb](https://github.com/clintonb)) - Updates to OpenIdConnectAuth [\#911](https://github.com/omab/python-social-auth/pull/911) ([clintonb](https://github.com/clintonb)) - Corrected default value of JSONField [\#908](https://github.com/omab/python-social-auth/pull/908) ([clintonb](https://github.com/clintonb)) @@ -23,6 +71,8 @@ **Merged pull requests:** +- ADDED: upwork backend [\#904](https://github.com/omab/python-social-auth/pull/904) ([shepilov-vladislav](https://github.com/shepilov-vladislav)) +- Add Sketchfab OAuth2 backend [\#901](https://github.com/omab/python-social-auth/pull/901) ([sylvinus](https://github.com/sylvinus)) - django 1.8+ compat to ensure to\_python is always called when accessing result from db.. [\#897](https://github.com/omab/python-social-auth/pull/897) ([sbussetti](https://github.com/sbussetti)) ## [v0.2.16](https://github.com/omab/python-social-auth/tree/v0.2.16) (2016-04-13) @@ -46,6 +96,9 @@ **Merged pull requests:** +- Add weixin public number oauth backend. [\#899](https://github.com/omab/python-social-auth/pull/899) ([duoduo369](https://github.com/duoduo369)) +- Add support for Untappd as an OAuth v2 backend [\#894](https://github.com/omab/python-social-auth/pull/894) ([svvitale](https://github.com/svvitale)) +- add coding oauth [\#892](https://github.com/omab/python-social-auth/pull/892) ([joway](https://github.com/joway)) - Add a backend for Classlink. [\#890](https://github.com/omab/python-social-auth/pull/890) ([antinescience](https://github.com/antinescience)) - Pass response to AuthCancel exception [\#883](https://github.com/omab/python-social-auth/pull/883) ([st4lk](https://github.com/st4lk)) - modifed wrong key names in pocket.py [\#878](https://github.com/omab/python-social-auth/pull/878) ([EunJung-Seo](https://github.com/EunJung-Seo)) @@ -57,6 +110,7 @@ - Add some tests for Spotify backend + add a backend for Deezer music service [\#845](https://github.com/omab/python-social-auth/pull/845) ([khamaileon](https://github.com/khamaileon)) - \[Fix\] update odnoklasniki docs to new domain ok [\#836](https://github.com/omab/python-social-auth/pull/836) ([vanadium23](https://github.com/vanadium23)) - add github enterprise docs on how to specify the API URL [\#834](https://github.com/omab/python-social-auth/pull/834) ([iserko](https://github.com/iserko)) +- Added optional 'include\_email' query param for Twitter backend. [\#829](https://github.com/omab/python-social-auth/pull/829) ([halfstrik](https://github.com/halfstrik)) - Fix ImportError: cannot import name ‘urlencode’ in Python3 [\#828](https://github.com/omab/python-social-auth/pull/828) ([mishbahr](https://github.com/mishbahr)) - Fix wrong evaluation of boolean kwargs [\#824](https://github.com/omab/python-social-auth/pull/824) ([falknes](https://github.com/falknes)) - SAML: raise AuthMissingParameter if idp param missing [\#821](https://github.com/omab/python-social-auth/pull/821) ([omarkhan](https://github.com/omarkhan)) @@ -85,6 +139,7 @@ - Add support for Drip Email Marketing Site [\#810](https://github.com/omab/python-social-auth/pull/810) ([buddylindsey](https://github.com/buddylindsey)) - Fix Django 1.10 deprecation warnings [\#806](https://github.com/omab/python-social-auth/pull/806) ([yprez](https://github.com/yprez)) +- bugs in social\_user and associate\_by\_email return values [\#800](https://github.com/omab/python-social-auth/pull/800) ([falcon1kr](https://github.com/falcon1kr)) - Changed instagram backend to new authorization routes [\#797](https://github.com/omab/python-social-auth/pull/797) ([clybob](https://github.com/clybob)) - Update settings.rst [\#793](https://github.com/omab/python-social-auth/pull/793) ([skolsuper](https://github.com/skolsuper)) - Add naver.com OAuth2 backend [\#789](https://github.com/omab/python-social-auth/pull/789) ([se0kjun](https://github.com/se0kjun)) @@ -427,7 +482,7 @@ - Pull Request for \#501 [\#502](https://github.com/omab/python-social-auth/pull/502) ([cdeblois](https://github.com/cdeblois)) - Add support for Launchpad OpenId [\#500](https://github.com/omab/python-social-auth/pull/500) ([ianw](https://github.com/ianw)) - Jawbone authentification fix [\#498](https://github.com/omab/python-social-auth/pull/498) ([rivf](https://github.com/rivf)) -- Coursera backend [\#496](https://github.com/omab/python-social-auth/pull/496) ([dreame4](https://github.com/dreame4)) +- Coursera backend [\#496](https://github.com/omab/python-social-auth/pull/496) ([adambabik](https://github.com/adambabik)) - Added nonce unique constraint [\#491](https://github.com/omab/python-social-auth/pull/491) ([candlejack297](https://github.com/candlejack297)) - Store Spotify's refresh\_token. [\#482](https://github.com/omab/python-social-auth/pull/482) ([ctbarna](https://github.com/ctbarna)) - Slack improvements [\#479](https://github.com/omab/python-social-auth/pull/479) ([gorillamania](https://github.com/gorillamania)) diff --git a/social/__init__.py b/social/__init__.py index 895ff9af9..ea23bbdcc 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 19) +version = (0, 2, 20) extra = '' __version__ = '.'.join(map(str, version)) + extra From f7d4cdcc3bb07e09b91c6572b3912f3562a2e5a2 Mon Sep 17 00:00:00 2001 From: Raphael Das Gupta Date: Fri, 12 Aug 2016 11:11:08 +0200 Subject: [PATCH 861/890] fix typo --- docs/pipeline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index bd6566047..67d25adef 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -12,7 +12,7 @@ in the parameters to avoid errors for unexpected arguments. Each pipeline entry can return a ``dict`` or ``None``, any other type of return value is treated as a response instance and returned directly to the client, -check *Partial Piepeline* below for details. +check *Partial Pipeline* below for details. If a ``dict`` is returned, the value in the set will be merged into the ``kwargs`` argument for the next pipeline entry, ``None`` is taken as if ``{}`` From b3cbdb835119aef8be3d7f9abb1146cc0dc17c36 Mon Sep 17 00:00:00 2001 From: an0o0nym Date: Sat, 13 Aug 2016 03:15:46 +0200 Subject: [PATCH 862/890] Rewrited pipeline.rst --- docs/pipeline.rst | 57 +++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/docs/pipeline.rst b/docs/pipeline.rst index bd6566047..5f4ba0771 100644 --- a/docs/pipeline.rst +++ b/docs/pipeline.rst @@ -63,7 +63,7 @@ The default pipeline is composed by:: # Create a user account if we haven't found one yet. 'social.pipeline.user.create_user', - # Create the record that associated the social account with this user. + # Create the record that associates the social account with the user. 'social.pipeline.social_auth.associate_user', # Populate the extra_data field in the social record with the values @@ -109,8 +109,9 @@ Each pipeline function will receive the following parameters: * ``is_new`` flag (initialized as ``False``) * Any arguments passed to ``auth_complete`` backend method, default views pass these arguments: - - current logged in user (if it's logged in, otherwise ``None``) - - current request + + * current logged in user (if it's logged in, otherwise ``None``) + * current request Disconnection Pipeline @@ -199,9 +200,9 @@ three fields: ``verified = True / False`` Flag marking if the email was verified or not. -You should use the code in this instance the build the link for email -validation which should go to ``/complete/email?verification_code=``, if using -Django you can do it with:: +You should use the code in this instance to build the link for email +validation which should go to ``/complete/email?verification_code=``. If you are using +Django, you can do it with:: from django.core.urlresolvers import reverse url = strategy.build_absolute_uri( @@ -226,23 +227,26 @@ Or individually by defining the setting per backend basis like Extending the Pipeline ====================== -The main purpose of the pipeline (either creation or deletion pipelines), is to -allow extensibility for developers, you can jump in the middle of it, do -changes to the data, create other models instances, ask users for data, or even -halt the whole process. +The main purpose of the pipeline (either creation or deletion pipelines) is to +allow extensibility for developers. You can jump in the middle of it, do +changes to the data, create other models instances, ask users for extra data, +or even halt the whole process. Extending the pipeline implies: 1. Writing a function - 2. Locate it in a accessible path (accessible in the way that it can be - imported) - 3. Override the default pipeline definition with one that includes your - function. - -Writing the function is quite simple. Depending on the place you locate it will -determine the arguments it will receive, for example, adding your function -after ``social.pipeline.user.create_user`` ensures that you get the user -instance (created or already existent) instead of a ``None`` value. + 2. Locating the function in an accessible path + (accessible in the way that it can be imported) + 3. Overriding the default pipeline definition with one that includes + newly created function. + +The part of writing the function is quite simple. However please be careful +when placing your function in the pipeline definition, because order +does matter in this case! Ordering of functions in ``SOCIAL_AUTH_PIPELINE`` +will determine the value of arguments that each function will receive. +For example, adding your function after ``social.pipeline.user.create_user`` +ensures that your function will get the user instance (created or already existent) +instead of a ``None`` value. The pipeline functions will get quite a lot of arguments, ranging from the backend in use, different model instances, server requests and provider @@ -285,7 +289,7 @@ other APIs endpoints to retrieve even more details about the user, store them on some other place, etc. Here's an example of a simple pipeline function that will create a ``Profile`` -class related to the current user, this profile will store some simple details +class instance, related to the current user. This profile will store some simple details returned by the provider (``Facebook`` in this example). The usual Facebook ``response`` looks like this:: @@ -319,9 +323,9 @@ the timezone in our ``Profile`` model:: profile.timezone = response.get('timezone') profile.save() -Now all that's needed is to tell ``python-social-auth`` to use this function in -the pipeline, since it needs the user instance, it needs to be put after -``create_user`` function:: +Now all that's needed is to tell ``python-social-auth`` to use our function in +the pipeline. Since the function uses user instance, we need to put it after +``social.pipeline.user.create_user``:: SOCIAL_AUTH_PIPELINE = ( 'social.pipeline.social_auth.social_details', @@ -336,10 +340,9 @@ the pipeline, since it needs the user instance, it needs to be put after 'social.pipeline.user.user_details', ) -If the return value of the function is a ``dict``, the values will be merged -into the next pipeline function parameters, so, for instance, if you want the -``profile`` instance to be available to the next function, all that it needs to -do is return ``{'profile': profile}``. +So far the function we created returns ``None``, which is taken as if ``{}`` was returned. +If you want the ``profile`` object to be available to the next function in the +pipeline, all you need to do is return ``{'profile': profile}``. .. _python-social-auth: https://github.com/omab/python-social-auth .. _example applications: https://github.com/omab/python-social-auth/tree/master/examples From 361f42235fe51f422af029f325d041407a16036c Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Sat, 13 Aug 2016 11:43:31 -0400 Subject: [PATCH 863/890] Fixed Django migrations The app rename in #916 breaks migrations for previously-setup systems. This commit informs Django that the migrations under the new app name replace the migrations under the old app name. Fixes #991 --- social/apps/django_app/default/migrations/0001_initial.py | 2 ++ .../apps/django_app/default/migrations/0002_add_related_name.py | 1 + .../default/migrations/0003_alter_email_max_length.py | 2 ++ .../django_app/default/migrations/0004_auto_20160423_0400.py | 1 + 4 files changed, 6 insertions(+) diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py index 9917f67c3..6766d9285 100644 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ b/social/apps/django_app/default/migrations/0001_initial.py @@ -23,6 +23,8 @@ class Migration(migrations.Migration): + replaces = [('default', '0001_initial')] + dependencies = [ migrations.swappable_dependency(USER_MODEL), ] diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py index 00595cec2..8a791eaea 100644 --- a/social/apps/django_app/default/migrations/0002_add_related_name.py +++ b/social/apps/django_app/default/migrations/0002_add_related_name.py @@ -12,6 +12,7 @@ class Migration(migrations.Migration): + replaces = [('default', '0002_add_related_name')] dependencies = [ ('social_auth', '0001_initial'), diff --git a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py index 2f5c86ea3..3557d707e 100644 --- a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py +++ b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py @@ -10,6 +10,8 @@ class Migration(migrations.Migration): + replaces = [('default', '0003_alter_email_max_length')] + dependencies = [ ('social_auth', '0002_add_related_name'), ] diff --git a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py index 3a1c0b089..82648bcb5 100644 --- a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py +++ b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py @@ -6,6 +6,7 @@ class Migration(migrations.Migration): + replaces = [('default', '0004_auto_20160423_0400')] dependencies = [ ('social_auth', '0003_alter_email_max_length'), From 61dfbc2ea7a897981a15db8904c5a68d471b0515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 15 Aug 2016 12:24:35 -0300 Subject: [PATCH 864/890] v0.2.21 --- CHANGELOG.md | 20 ++++++++++++++++++-- social/__init__.py | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6effa10ea..c16a9aab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,24 @@ # Change Log -## [v0.2.20](https://github.com/omab/python-social-auth/tree/v0.2.20) (2016-08-11) +## [Unreleased](https://github.com/omab/python-social-auth/tree/HEAD) -[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.19...HEAD) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.21...HEAD) + +## [v0.2.21](https://github.com/omab/python-social-auth/tree/v0.2.21) (2016-08-15) + +**Closed issues:** + +- Django Migrations Broken [\#991](https://github.com/omab/python-social-auth/issues/991) + +**Merged pull requests:** + +- Fixed Django Migrations [\#993](https://github.com/omab/python-social-auth/pull/993) ([clintonb](https://github.com/clintonb)) +- Rewrited pipeline.rst [\#992](https://github.com/omab/python-social-auth/pull/992) ([an0o0nym](https://github.com/an0o0nym)) +- fix typo "Piepeline" -\> "Pipeline" [\#990](https://github.com/omab/python-social-auth/pull/990) ([das-g](https://github.com/das-g)) +- Fixed Django \< 1.8 broken compatibility [\#986](https://github.com/omab/python-social-auth/pull/986) ([seroy](https://github.com/seroy)) + +## [v0.2.20](https://github.com/omab/python-social-auth/tree/v0.2.20) (2016-08-12) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.19...v0.2.20) **Closed issues:** diff --git a/social/__init__.py b/social/__init__.py index ea23bbdcc..8766fd5a1 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 20) +version = (0, 2, 21) extra = '' __version__ = '.'.join(map(str, version)) + extra From 8ad3234cfa6a44bbc2fae7b6a5224285a2c20e74 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 18 Aug 2016 18:28:53 +0400 Subject: [PATCH 865/890] Fix example for email auth --- docs/backends/email.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backends/email.rst b/docs/backends/email.rst index 635f9f5a9..9e11b3d6c 100644 --- a/docs/backends/email.rst +++ b/docs/backends/email.rst @@ -41,8 +41,8 @@ Password handling Here's an example of password handling to add to the pipeline:: - def user_password(strategy, user, is_new=False, *args, **kwargs): - if strategy.backend.name != 'email': + def user_password(strategy, backend, user, is_new=False, *args, **kwargs): + if backend.name != 'email': return password = strategy.request_data()['password'] @@ -51,7 +51,7 @@ Here's an example of password handling to add to the pipeline:: user.save() elif not user.validate_password(password): # return {'user': None, 'social': None} - raise AuthException(strategy.backend) + raise AuthForbidden(backend) .. _python-social-auth: https://github.com/omab/python-social-auth .. _EmailAuth: https://github.com/omab/python-social-auth/blob/master/social/backends/email.py#L5 From 9aa8e9eb36381f70e0146c10060da120078fd01f Mon Sep 17 00:00:00 2001 From: Josh Schneier Date: Mon, 3 Oct 2016 17:36:28 -0400 Subject: [PATCH 866/890] Subclass MiddlewareMixin on Django 1.10 --- social/apps/django_app/middleware.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index e9aaebab5..fefa6b230 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -10,8 +10,13 @@ from social.exceptions import SocialAuthBaseException from social.utils import social_logger +try: + from django.utils.deprecation import MiddlewareMixin +except ImportError: + MiddlewareMixin = object -class SocialAuthExceptionMiddleware(object): + +class SocialAuthExceptionMiddleware(MiddlewareMixin): """Middleware that handles Social Auth AuthExceptions by providing the user with a message, logging an error, and redirecting to some next location. From be929d8ca7a62ec9ff1f1bed426855b1a2d00ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 5 Nov 2016 09:46:59 -0300 Subject: [PATCH 867/890] Cleanup and depend on social modules --- Dockerfile | 5 + Makefile | 25 +- requirements-python3.txt | 14 + requirements.txt | 7 +- social/actions.py | 123 +---- social/apps/cherrypy_app/models.py | 58 +-- social/apps/cherrypy_app/utils.py | 52 +-- social/apps/cherrypy_app/views.py | 30 +- social/apps/django_app/__init__.py | 21 - social/apps/django_app/context_processors.py | 47 +- social/apps/django_app/default/__init__.py | 10 - social/apps/django_app/default/admin.py | 63 +-- social/apps/django_app/default/config.py | 16 +- social/apps/django_app/default/fields.py | 89 +--- social/apps/django_app/default/managers.py | 13 +- social/apps/django_app/default/migrations.py | 1 + .../default/migrations/0001_initial.py | 117 ----- .../migrations/0002_add_related_name.py | 27 -- .../migrations/0003_alter_email_max_length.py | 25 - .../migrations/0004_auto_20160423_0400.py | 21 - .../migrations/0005_auto_20160727_2333.py | 19 - .../django_app/default/migrations/__init__.py | 0 social/apps/django_app/default/models.py | 123 +---- social/apps/django_app/default/tests.py | 1 - social/apps/django_app/me/__init__.py | 9 - social/apps/django_app/me/config.py | 15 +- social/apps/django_app/me/models.py | 78 +--- social/apps/django_app/me/tests.py | 1 - social/apps/django_app/middleware.py | 63 +-- social/apps/django_app/tests.py | 51 -- social/apps/django_app/urls.py | 27 +- social/apps/django_app/utils.py | 75 +-- social/apps/django_app/views.py | 60 +-- social/apps/flask_app/__init__.py | 5 - social/apps/flask_app/default/models.py | 78 +--- social/apps/flask_app/me/models.py | 46 +- social/apps/flask_app/peewee/models.py | 49 +- social/apps/flask_app/routes.py | 46 +- social/apps/flask_app/template_filters.py | 26 +- social/apps/flask_app/utils.py | 54 +-- social/apps/pyramid_app/__init__.py | 14 +- social/apps/pyramid_app/models.py | 65 +-- social/apps/pyramid_app/utils.py | 83 +--- social/apps/pyramid_app/views.py | 31 +- social/apps/tornado_app/handlers.py | 51 +- social/apps/tornado_app/models.py | 62 +-- social/apps/tornado_app/routes.py | 13 - social/apps/tornado_app/utils.py | 50 +- social/apps/webpy_app/__init__.py | 5 - social/apps/webpy_app/app.py | 74 +-- social/apps/webpy_app/models.py | 63 +-- social/apps/webpy_app/utils.py | 70 +-- social/backends/amazon.py | 46 +- social/backends/angel.py | 31 +- social/backends/aol.py | 11 +- social/backends/appsfuel.py | 43 +- social/backends/arcgis.py | 34 +- social/backends/azuread.py | 121 +---- social/backends/base.py | 239 +--------- social/backends/battlenet.py | 51 +- social/backends/beats.py | 66 +-- social/backends/behance.py | 41 +- social/backends/belgiumeid.py | 12 +- social/backends/bitbucket.py | 104 +---- social/backends/box.py | 56 +-- social/backends/changetip.py | 28 +- social/backends/classlink.py | 45 +- social/backends/clef.py | 55 +-- social/backends/coding.py | 49 +- social/backends/coinbase.py | 36 +- social/backends/coursera.py | 44 +- social/backends/dailymotion.py | 25 +- social/backends/deezer.py | 50 +- social/backends/digitalocean.py | 42 +- social/backends/disqus.py | 52 +-- social/backends/docker.py | 47 +- social/backends/douban.py | 60 +-- social/backends/dribbble.py | 63 +-- social/backends/drip.py | 26 +- social/backends/dropbox.py | 68 +-- social/backends/echosign.py | 25 +- social/backends/edmodo.py | 35 +- social/backends/email.py | 13 +- social/backends/eveonline.py | 42 +- social/backends/evernote.py | 76 +-- social/backends/exacttarget.py | 105 +---- social/backends/facebook.py | 212 +-------- social/backends/fedora.py | 12 +- social/backends/fitbit.py | 66 +-- social/backends/flickr.py | 44 +- social/backends/foursquare.py | 37 +- social/backends/gae.py | 41 +- social/backends/github.py | 121 +---- social/backends/github_enterprise.py | 45 +- social/backends/goclio.py | 36 +- social/backends/goclioeu.py | 15 +- social/backends/google.py | 215 +-------- social/backends/instagram.py | 54 +-- social/backends/itembase.py | 86 +--- social/backends/jawbone.py | 78 +--- social/backends/justgiving.py | 57 +-- social/backends/kakao.py | 41 +- social/backends/khanacademy.py | 127 +---- social/backends/lastfm.py | 60 +-- social/backends/launchpad.py | 12 +- social/backends/legacy.py | 45 +- social/backends/line.py | 92 +--- social/backends/linkedin.py | 98 +--- social/backends/live.py | 45 +- social/backends/livejournal.py | 27 +- social/backends/loginradius.py | 70 +-- social/backends/mailru.py | 46 +- social/backends/mapmyfitness.py | 50 +- social/backends/meetup.py | 35 +- social/backends/mendeley.py | 69 +-- social/backends/mineid.py | 39 +- social/backends/mixcloud.py | 27 +- social/backends/moves.py | 31 +- social/backends/nationbuilder.py | 49 +- social/backends/naver.py | 60 +-- social/backends/ngpvan.py | 67 +-- social/backends/nk.py | 75 +-- social/backends/oauth.py | 435 +----------------- social/backends/odnoklassniki.py | 174 +------ social/backends/open_id.py | 381 +-------------- social/backends/openstreetmap.py | 58 +-- social/backends/orbi.py | 43 +- social/backends/persona.py | 51 +- social/backends/pinterest.py | 47 +- social/backends/pixelpin.py | 34 +- social/backends/pocket.py | 46 +- social/backends/podio.py | 39 +- social/backends/professionali.py | 56 +-- social/backends/pushbullet.py | 24 +- social/backends/qiita.py | 67 +-- social/backends/qq.py | 71 +-- social/backends/rdio.py | 73 +-- social/backends/readability.py | 36 +- social/backends/reddit.py | 54 +-- social/backends/runkeeper.py | 48 +- social/backends/salesforce.py | 50 +- social/backends/saml.py | 328 +------------ social/backends/shopify.py | 93 +--- social/backends/sketchfab.py | 40 +- social/backends/skyrock.py | 33 +- social/backends/slack.py | 67 +-- social/backends/soundcloud.py | 56 +-- social/backends/spotify.py | 48 +- social/backends/stackoverflow.py | 44 +- social/backends/steam.py | 48 +- social/backends/stocktwits.py | 38 +- social/backends/strava.py | 47 +- social/backends/stripe.py | 55 +-- social/backends/suse.py | 18 +- social/backends/taobao.py | 27 +- social/backends/thisismyjam.py | 34 +- social/backends/trello.py | 48 +- social/backends/tripit.py | 44 +- social/backends/tumblr.py | 32 +- social/backends/twilio.py | 40 +- social/backends/twitch.py | 31 +- social/backends/twitter.py | 42 +- social/backends/uber.py | 40 +- social/backends/ubuntu.py | 17 +- social/backends/untappd.py | 111 +---- social/backends/upwork.py | 40 +- social/backends/username.py | 12 +- social/backends/utils.py | 82 +--- social/backends/vend.py | 40 +- social/backends/vimeo.py | 80 +--- social/backends/vk.py | 210 +-------- social/backends/weibo.py | 62 +-- social/backends/weixin.py | 178 +------ social/backends/withings.py | 15 +- social/backends/wunderlist.py | 30 +- social/backends/xing.py | 46 +- social/backends/yahoo.py | 160 +------ social/backends/yammer.py | 45 +- social/backends/yandex.py | 79 +--- social/backends/zotero.py | 30 +- social/exceptions.py | 118 +---- social/pipeline/__init__.py | 61 +-- social/pipeline/debug.py | 14 +- social/pipeline/disconnect.py | 33 +- social/pipeline/mail.py | 25 +- social/pipeline/partial.py | 22 +- social/pipeline/social_auth.py | 92 +--- social/pipeline/user.py | 97 +--- social/pipeline/utils.py | 63 +-- social/storage/base.py | 259 +---------- social/storage/django_orm.py | 175 +------ social/storage/mongoengine_orm.py | 191 +------- social/storage/peewee_orm.py | 201 +------- social/storage/sqlalchemy_orm.py | 241 +--------- social/store.py | 85 +--- social/strategies/base.py | 213 +-------- social/strategies/cherrypy_strategy.py | 68 +-- social/strategies/django_strategy.py | 147 +----- social/strategies/flask_strategy.py | 57 +-- social/strategies/pyramid_strategy.py | 75 +-- social/strategies/tornado_strategy.py | 76 +-- social/strategies/utils.py | 28 +- social/strategies/webpy_strategy.py | 66 +-- social/tests/requirements-pypy.txt | 3 +- social/tests/requirements-python3.txt | 3 +- social/tests/requirements.txt | 3 +- social/utils.py | 271 +---------- tox.ini | 5 +- 208 files changed, 277 insertions(+), 12988 deletions(-) create mode 100644 Dockerfile create mode 100644 social/apps/django_app/default/migrations.py delete mode 100644 social/apps/django_app/default/migrations/0001_initial.py delete mode 100644 social/apps/django_app/default/migrations/0002_add_related_name.py delete mode 100644 social/apps/django_app/default/migrations/0003_alter_email_max_length.py delete mode 100644 social/apps/django_app/default/migrations/0004_auto_20160423_0400.py delete mode 100644 social/apps/django_app/default/migrations/0005_auto_20160727_2333.py delete mode 100644 social/apps/django_app/default/migrations/__init__.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..6ce7af21a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM n42org/tox +MAINTAINER Matías Aguirre +RUN apt-get update +RUN apt-get install -y make libssl-dev libxml2-dev libxmlsec1-dev mongodb-server mongodb-dev swig openssl +RUN ln -s /usr/include/x86_64-linux-gnu/openssl/opensslconf.h /usr/include/openssl/opensslconf.h diff --git a/Makefile b/Makefile index 03494f7f3..7b91f4d55 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,28 @@ publish: python setup.py bdist_wheel --python-tag py2 upload BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3 upload +run-tox: + @ tox + +docker-tox-build: + @ docker build -t omab/psa-legacy . + +docker-tox: docker-tox-build + @ docker run -it --rm \ + --name psa-legacy-test \ + -v "`pwd`:/code" \ + -w /code omab/psa-legacy tox + +docker-shell: docker-tox-build + @ docker run -it --rm \ + --name psa-legacy-test \ + -v "`pwd`:/code" \ + -w /code omab/psa-legacy bash + clean: - find . -name '*.py[co]' -delete - find . -name '__pycache__' -delete - rm -rf python_social_auth.egg-info dist build + @ find . -name '*.py[co]' -delete + @ find . -name '__pycache__' -delete + @ rm -rf *.egg-info dist build + .PHONY: site docs publish diff --git a/requirements-python3.txt b/requirements-python3.txt index b834c7f82..a126ab21b 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -4,3 +4,17 @@ oauthlib>=1.0.3 requests-oauthlib>=0.6.1 six>=1.10.0 PyJWT>=1.4.0 +social-auth-core +social-auth-storage-mongoengine +social-auth-storage-peewee +social-auth-storage-sqlalchemy +social-auth-app-cherrypy +social-auth-app-django +social-auth-app-django-mongoengine +social-auth-app-flask +social-auth-app-flask-mongoengine +social-auth-app-flask-peewee +social-auth-app-flask-sqlalchemy +social-auth-app-pyramid +social-auth-app-tornado +social-auth-app-webpy diff --git a/requirements.txt b/requirements.txt index aa94fffd1..4e8d6b68a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1 @@ -python-openid>=2.2.5 -requests>=2.9.1 -oauthlib>=1.0.3 -requests-oauthlib>=0.6.1 -six>=1.10.0 -PyJWT>=1.4.0 +social-auth-core diff --git a/social/actions.py b/social/actions.py index af61bd733..364dc99ef 100644 --- a/social/actions.py +++ b/social/actions.py @@ -1,122 +1 @@ -from social.p3 import quote -from social.utils import sanitize_redirect, user_is_authenticated, \ - user_is_active, partial_pipeline_data, setting_url - - -def do_auth(backend, redirect_name='next'): - # Clean any partial pipeline data - backend.strategy.clean_partial_pipeline() - - # Save any defined next value into session - data = backend.strategy.request_data(merge=False) - - # Save extra data into session. - for field_name in backend.setting('FIELDS_STORED_IN_SESSION', []): - if field_name in data: - backend.strategy.session_set(field_name, data[field_name]) - - if redirect_name in data: - # Check and sanitize a user-defined GET/POST next field value - redirect_uri = data[redirect_name] - if backend.setting('SANITIZE_REDIRECTS', True): - allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \ - [backend.strategy.request_host()] - redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri) - backend.strategy.session_set( - redirect_name, - redirect_uri or backend.setting('LOGIN_REDIRECT_URL') - ) - return backend.start() - - -def do_complete(backend, login, user=None, redirect_name='next', - *args, **kwargs): - data = backend.strategy.request_data() - - is_authenticated = user_is_authenticated(user) - user = is_authenticated and user or None - - partial = partial_pipeline_data(backend, user, *args, **kwargs) - if partial: - xargs, xkwargs = partial - user = backend.continue_pipeline(*xargs, **xkwargs) - else: - user = backend.complete(user=user, *args, **kwargs) - - # pop redirect value before the session is trashed on login(), but after - # the pipeline so that the pipeline can change the redirect if needed - redirect_value = backend.strategy.session_get(redirect_name, '') or \ - data.get(redirect_name, '') - - user_model = backend.strategy.storage.user.user_model() - if user and not isinstance(user, user_model): - return user - - if is_authenticated: - if not user: - url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL') - else: - url = setting_url(backend, redirect_value, - 'NEW_ASSOCIATION_REDIRECT_URL', - 'LOGIN_REDIRECT_URL') - elif user: - if user_is_active(user): - # catch is_new/social_user in case login() resets the instance - is_new = getattr(user, 'is_new', False) - social_user = user.social_user - login(backend, user, social_user) - # store last login backend name in session - backend.strategy.session_set('social_auth_last_login_backend', - social_user.provider) - - if is_new: - url = setting_url(backend, - 'NEW_USER_REDIRECT_URL', - redirect_value, - 'LOGIN_REDIRECT_URL') - else: - url = setting_url(backend, redirect_value, - 'LOGIN_REDIRECT_URL') - else: - if backend.setting('INACTIVE_USER_LOGIN', False): - social_user = user.social_user - login(backend, user, social_user) - url = setting_url(backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', - 'LOGIN_URL') - else: - url = setting_url(backend, 'LOGIN_ERROR_URL', 'LOGIN_URL') - - if redirect_value and redirect_value != url: - redirect_value = quote(redirect_value) - url += ('?' in url and '&' or '?') + \ - '{0}={1}'.format(redirect_name, redirect_value) - - if backend.setting('SANITIZE_REDIRECTS', True): - allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \ - [backend.strategy.request_host()] - url = sanitize_redirect(allowed_hosts, url) or \ - backend.setting('LOGIN_REDIRECT_URL') - return backend.strategy.redirect(url) - - -def do_disconnect(backend, user, association_id=None, redirect_name='next', - *args, **kwargs): - partial = partial_pipeline_data(backend, user, *args, **kwargs) - if partial: - xargs, xkwargs = partial - if association_id and not xkwargs.get('association_id'): - xkwargs['association_id'] = association_id - response = backend.disconnect(*xargs, **xkwargs) - else: - response = backend.disconnect(user=user, association_id=association_id, - *args, **kwargs) - - if isinstance(response, dict): - response = backend.strategy.redirect( - backend.strategy.absolute_uri( - backend.strategy.request_data().get(redirect_name, '') or - backend.setting('DISCONNECT_REDIRECT_URL') or - backend.setting('LOGIN_REDIRECT_URL') - ) - ) - return response +from social_core.actions import do_auth, do_complete, do_disconnect diff --git a/social/apps/cherrypy_app/models.py b/social/apps/cherrypy_app/models.py index d0c7806b0..e9056fee0 100644 --- a/social/apps/cherrypy_app/models.py +++ b/social/apps/cherrypy_app/models.py @@ -1,57 +1 @@ -"""Flask SQLAlchemy ORM models for Social Auth""" -import cherrypy - -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.orm import relationship -from sqlalchemy.ext.declarative import declarative_base - -from social.utils import setting_name, module_member -from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ - SQLAlchemyAssociationMixin, \ - SQLAlchemyNonceMixin, \ - BaseSQLAlchemyStorage - - -SocialBase = declarative_base() - -DB_SESSION_ATTR = cherrypy.config.get(setting_name('DB_SESSION_ATTR'), 'db') -UID_LENGTH = cherrypy.config.get(setting_name('UID_LENGTH'), 255) -User = module_member(cherrypy.config[setting_name('USER_MODEL')]) - - -class CherryPySocialBase(object): - @classmethod - def _session(cls): - return getattr(cherrypy.request, DB_SESSION_ATTR) - - -class UserSocialAuth(CherryPySocialBase, SQLAlchemyUserMixin, SocialBase): - """Social Auth association model""" - uid = Column(String(UID_LENGTH)) - user_id = Column(Integer, ForeignKey(User.id), - nullable=False, index=True) - user = relationship(User, backref='social_auth') - - @classmethod - def username_max_length(cls): - return User.__table__.columns.get('username').type.length - - @classmethod - def user_model(cls): - return User - - -class Nonce(CherryPySocialBase, SQLAlchemyNonceMixin, SocialBase): - """One use numbers""" - pass - - -class Association(CherryPySocialBase, SQLAlchemyAssociationMixin, SocialBase): - """OpenId account association""" - pass - - -class CherryPyStorage(BaseSQLAlchemyStorage): - user = UserSocialAuth - nonce = Nonce - association = Association +from social_cherrypy.models import CherryPySocialBase, UserSocialAuth, Nonce, Association, CherryPyStorage diff --git a/social/apps/cherrypy_app/utils.py b/social/apps/cherrypy_app/utils.py index 70e89c919..31e62f858 100644 --- a/social/apps/cherrypy_app/utils.py +++ b/social/apps/cherrypy_app/utils.py @@ -1,51 +1 @@ -import warnings -from functools import wraps - -import cherrypy - -from social.utils import setting_name, module_member -from social.strategies.utils import get_strategy -from social.backends.utils import get_backend, user_backends_data - - -DEFAULTS = { - 'STRATEGY': 'social.strategies.cherrypy_strategy.CherryPyStrategy', - 'STORAGE': 'social.apps.cherrypy_app.models.CherryPyStorage' -} - - -def get_helper(name): - return cherrypy.config.get(setting_name(name), DEFAULTS.get(name, None)) - - -def load_backend(strategy, name, redirect_uri): - backends = get_helper('AUTHENTICATION_BACKENDS') - Backend = get_backend(backends, name) - return Backend(strategy=strategy, redirect_uri=redirect_uri) - - -def psa(redirect_uri=None): - def decorator(func): - @wraps(func) - def wrapper(self, backend=None, *args, **kwargs): - uri = redirect_uri - if uri and backend and '%(backend)s' in uri: - uri = uri % {'backend': backend} - self.strategy = get_strategy(get_helper('STRATEGY'), - get_helper('STORAGE')) - self.backend = load_backend(self.strategy, backend, uri) - return func(self, backend, *args, **kwargs) - return wrapper - return decorator - - -def backends(user): - """Load Social Auth current user data to context under the key 'backends'. - Will return the output of social.backends.utils.user_backends_data.""" - return user_backends_data(user, get_helper('AUTHENTICATION_BACKENDS'), - module_member(get_helper('STORAGE'))) - - -def strategy(*args, **kwargs): - warnings.warn('@strategy decorator is deprecated, use @psa instead') - return psa(*args, **kwargs) +from social_cherrypy.utils import get_helper, load_backend, psa, backends, strategy diff --git a/social/apps/cherrypy_app/views.py b/social/apps/cherrypy_app/views.py index 940868f45..ce14e3542 100644 --- a/social/apps/cherrypy_app/views.py +++ b/social/apps/cherrypy_app/views.py @@ -1,29 +1 @@ -import cherrypy - -from social.utils import setting_name, module_member -from social.actions import do_auth, do_complete, do_disconnect -from social.apps.cherrypy_app.utils import psa - - -class CherryPyPSAViews(object): - @cherrypy.expose - @psa('/complete/%(backend)s') - def login(self, backend): - return do_auth(self.backend) - - @cherrypy.expose - @psa('/complete/%(backend)s') - def complete(self, backend, *args, **kwargs): - login = cherrypy.config.get(setting_name('LOGIN_METHOD')) - do_login = module_member(login) if login else self.do_login - user = getattr(cherrypy.request, 'user', None) - return do_complete(self.backend, do_login, user=user, *args, **kwargs) - - @cherrypy.expose - @psa() - def disconnect(self, backend, association_id=None): - user = getattr(cherrypy.request, 'user', None) - return do_disconnect(self.backend, user, association_id) - - def do_login(self, backend, user, social_user): - backend.strategy.session_set('user_id', user.id) +from social_cherrypy.views import CherryPyPSAViews diff --git a/social/apps/django_app/__init__.py b/social/apps/django_app/__init__.py index 217d3cea8..e69de29bb 100644 --- a/social/apps/django_app/__init__.py +++ b/social/apps/django_app/__init__.py @@ -1,21 +0,0 @@ -""" -Django framework support. - -To use this: - * Add 'social.apps.django_app.default' if using default ORM, - or 'social.apps.django_app.me' if using mongoengine - * Add url('', include('social.apps.django_app.urls', namespace='social')) to - urls.py - * Define SOCIAL_AUTH_STORAGE and SOCIAL_AUTH_STRATEGY, default values: - SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' - SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' -""" -import django - - -if django.VERSION[0] == 1 and django.VERSION[1] < 7: - from social.strategies.utils import set_current_strategy_getter - from social.apps.django_app.utils import load_strategy - # Set strategy loader method to workaround current strategy getter needed on - # get_user() method on authentication backends when working with Django - set_current_strategy_getter(load_strategy) diff --git a/social/apps/django_app/context_processors.py b/social/apps/django_app/context_processors.py index fb70e60ce..691b9e95b 100644 --- a/social/apps/django_app/context_processors.py +++ b/social/apps/django_app/context_processors.py @@ -1,46 +1 @@ -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.utils.functional import SimpleLazyObject - -try: - from django.utils.functional import empty as _empty - empty = _empty -except ImportError: # django < 1.4 - empty = None - - -from social.backends.utils import user_backends_data -from social.apps.django_app.utils import Storage, BACKENDS - - -class LazyDict(SimpleLazyObject): - """Lazy dict initialization.""" - def __getitem__(self, name): - if self._wrapped is empty: - self._setup() - return self._wrapped[name] - - def __setitem__(self, name, value): - if self._wrapped is empty: - self._setup() - self._wrapped[name] = value - - -def backends(request): - """Load Social Auth current user data to context under the key 'backends'. - Will return the output of social.backends.utils.user_backends_data.""" - return {'backends': LazyDict(lambda: user_backends_data(request.user, - BACKENDS, - Storage))} - - -def login_redirect(request): - """Load current redirect to context.""" - value = request.method == 'POST' and \ - request.POST.get(REDIRECT_FIELD_NAME) or \ - request.GET.get(REDIRECT_FIELD_NAME) - querystring = value and (REDIRECT_FIELD_NAME + '=' + value) or '' - return { - 'REDIRECT_FIELD_NAME': REDIRECT_FIELD_NAME, - 'REDIRECT_FIELD_VALUE': value, - 'REDIRECT_QUERYSTRING': querystring - } +from social_django.context_processors import LazyDict, backends, login_redirect diff --git a/social/apps/django_app/default/__init__.py b/social/apps/django_app/default/__init__.py index 99c8c8e4d..e69de29bb 100644 --- a/social/apps/django_app/default/__init__.py +++ b/social/apps/django_app/default/__init__.py @@ -1,10 +0,0 @@ -""" -Django default ORM backend support. - -To enable this app: - * Add 'social.apps.django_app.default' to INSTALLED_APPS - * In urls.py include url('', include('social.apps.django_app.urls')) -""" - -default_app_config = \ - 'social.apps.django_app.default.config.PythonSocialAuthConfig' diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py index 5cee753fd..5807d303e 100644 --- a/social/apps/django_app/default/admin.py +++ b/social/apps/django_app/default/admin.py @@ -1,62 +1 @@ -"""Admin settings""" -from itertools import chain - -from django.conf import settings -from django.contrib import admin - -from social.utils import setting_name -from social.apps.django_app.default.models import UserSocialAuth, Nonce, \ - Association - - -class UserSocialAuthOption(admin.ModelAdmin): - """Social Auth user options""" - list_display = ('user', 'id', 'provider', 'uid') - list_filter = ('provider',) - raw_id_fields = ('user',) - list_select_related = True - - def get_search_fields(self, request=None): - search_fields = getattr( - settings, setting_name('ADMIN_USER_SEARCH_FIELDS'), None - ) - if search_fields is None: - _User = UserSocialAuth.user_model() - username = getattr(_User, 'USERNAME_FIELD', None) or \ - hasattr(_User, 'username') and 'username' or \ - None - fieldnames = ('first_name', 'last_name', 'email', username) - all_names = self._get_all_field_names(_User._meta) - search_fields = [name for name in fieldnames - if name and name in all_names] - return ['user__' + name for name in search_fields] - - @staticmethod - def _get_all_field_names(model): - names = chain.from_iterable( - (field.name, field.attname) - if hasattr(field, 'attname') else (field.name,) - for field in model.get_fields() - # For complete backwards compatibility, you may want to exclude - # GenericForeignKey from the results. - if not (field.many_to_one and field.related_model is None) - ) - return list(set(names)) - - -class NonceOption(admin.ModelAdmin): - """Nonce options""" - list_display = ('id', 'server_url', 'timestamp', 'salt') - search_fields = ('server_url',) - - -class AssociationOption(admin.ModelAdmin): - """Association options""" - list_display = ('id', 'server_url', 'assoc_type') - list_filter = ('assoc_type',) - search_fields = ('server_url',) - - -admin.site.register(UserSocialAuth, UserSocialAuthOption) -admin.site.register(Nonce, NonceOption) -admin.site.register(Association, AssociationOption) +from social_django.admin import UserSocialAuthOption, NonceOption, AssociationOption diff --git a/social/apps/django_app/default/config.py b/social/apps/django_app/default/config.py index a2c44b727..293d7ca22 100644 --- a/social/apps/django_app/default/config.py +++ b/social/apps/django_app/default/config.py @@ -1,15 +1 @@ -from django.apps import AppConfig - - -class PythonSocialAuthConfig(AppConfig): - name = 'social.apps.django_app.default' - label = 'social_auth' - verbose_name = 'Python Social Auth' - - def ready(self): - from social.strategies.utils import set_current_strategy_getter - from social.apps.django_app.utils import load_strategy - # Set strategy loader method to workaround current strategy getter - # needed on get_user() method on authentication backends when working - # with Django - set_current_strategy_getter(load_strategy) +from social_django.config import PythonSocialAuthConfig diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py index ab47fba91..3cc069190 100644 --- a/social/apps/django_app/default/fields.py +++ b/social/apps/django_app/default/fields.py @@ -1,88 +1 @@ -import json -import six -import functools - -from django.core.exceptions import ValidationError -from django.db import models - -try: - from django.utils.encoding import smart_unicode as smart_text - smart_text # placate pyflakes -except ImportError: - from django.utils.encoding import smart_text - -try: - from django.db.models import SubfieldBase - field_class = functools.partial(six.with_metaclass, SubfieldBase) -except ImportError: - field_class = functools.partial(six.with_metaclass, type) - - -class JSONField(field_class(models.TextField)): - """Simple JSON field that stores python structures as JSON strings - on database. - """ - - def __init__(self, *args, **kwargs): - kwargs.setdefault('default', {}) - super(JSONField, self).__init__(*args, **kwargs) - - def from_db_value(self, value, expression, connection, context): - return self.to_python(value) - - def to_python(self, value): - """ - Convert the input JSON value into python structures, raises - django.core.exceptions.ValidationError if the data can't be converted. - """ - if self.blank and not value: - return {} - value = value or '{}' - if isinstance(value, six.binary_type): - value = six.text_type(value, 'utf-8') - if isinstance(value, six.string_types): - try: - # with django 1.6 i have '"{}"' as default value here - if value[0] == value[-1] == '"': - value = value[1:-1] - - return json.loads(value) - except Exception as err: - raise ValidationError(str(err)) - else: - return value - - def validate(self, value, model_instance): - """Check value is a valid JSON string, raise ValidationError on - error.""" - if isinstance(value, six.string_types): - super(JSONField, self).validate(value, model_instance) - try: - json.loads(value) - except Exception as err: - raise ValidationError(str(err)) - - def get_prep_value(self, value): - """Convert value to JSON string before save""" - try: - return json.dumps(value) - except Exception as err: - raise ValidationError(str(err)) - - def value_to_string(self, obj): - """Return value from object converted to string properly""" - return smart_text(self.get_prep_value(self._get_val_from_obj(obj))) - - def value_from_object(self, obj): - """Return value dumped to string.""" - return self.get_prep_value(self._get_val_from_obj(obj)) - - -try: - from south.modelsinspector import add_introspection_rules - add_introspection_rules( - [], - ["^social\.apps\.django_app\.default\.fields\.JSONField"] - ) -except: - pass +from social_django.fields import JSONField diff --git a/social/apps/django_app/default/managers.py b/social/apps/django_app/default/managers.py index 5e8769d7f..4d71dda7d 100644 --- a/social/apps/django_app/default/managers.py +++ b/social/apps/django_app/default/managers.py @@ -1,12 +1 @@ -from django.db import models - - -class UserSocialAuthManager(models.Manager): - """Manager for the UserSocialAuth django model.""" - - def get_social_auth(self, provider, uid): - try: - return self.select_related('user').get(provider=provider, - uid=uid) - except self.model.DoesNotExist: - return None +from social_django.managers import UserSocialAuthManager diff --git a/social/apps/django_app/default/migrations.py b/social/apps/django_app/default/migrations.py new file mode 100644 index 000000000..13e762fd6 --- /dev/null +++ b/social/apps/django_app/default/migrations.py @@ -0,0 +1 @@ +from social_django import migrations diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py deleted file mode 100644 index 6766d9285..000000000 --- a/social/apps/django_app/default/migrations/0001_initial.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import social.apps.django_app.default.fields -from django.conf import settings -import social.storage.django_orm -from social.utils import setting_name - -USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ - getattr(settings, 'AUTH_USER_MODEL', None) or \ - 'auth.User' -UID_LENGTH = getattr(settings, setting_name('UID_LENGTH'), 255) -NONCE_SERVER_URL_LENGTH = getattr( - settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255 -) -ASSOCIATION_SERVER_URL_LENGTH = getattr( - settings, setting_name('ASSOCIATION_SERVER_URL_LENGTH'), 255 -) -ASSOCIATION_HANDLE_LENGTH = getattr( - settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255 -) - - -class Migration(migrations.Migration): - replaces = [('default', '0001_initial')] - - dependencies = [ - migrations.swappable_dependency(USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Association', - fields=[ - ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, - primary_key=True)), - ('server_url', - models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH)), - ('handle', - models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH)), - ('secret', models.CharField(max_length=255)), - ('issued', models.IntegerField()), - ('lifetime', models.IntegerField()), - ('assoc_type', models.CharField(max_length=64)), - ], - options={ - 'db_table': 'social_auth_association', - }, - bases=( - models.Model, social.storage.django_orm.DjangoAssociationMixin - ), - ), - migrations.CreateModel( - name='Code', - fields=[ - ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, - primary_key=True)), - ('email', models.EmailField(max_length=75)), - ('code', models.CharField(max_length=32, db_index=True)), - ('verified', models.BooleanField(default=False)), - ], - options={ - 'db_table': 'social_auth_code', - }, - bases=(models.Model, social.storage.django_orm.DjangoCodeMixin), - ), - migrations.CreateModel( - name='Nonce', - fields=[ - ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, - primary_key=True - )), - ('server_url', - models.CharField(max_length=NONCE_SERVER_URL_LENGTH)), - ('timestamp', models.IntegerField()), - ('salt', models.CharField(max_length=65)), - ], - options={ - 'db_table': 'social_auth_nonce', - }, - bases=(models.Model, social.storage.django_orm.DjangoNonceMixin), - ), - migrations.CreateModel( - name='UserSocialAuth', - fields=[ - ('id', models.AutoField( - verbose_name='ID', serialize=False, auto_created=True, - primary_key=True)), - ('provider', models.CharField(max_length=32)), - ('uid', models.CharField(max_length=UID_LENGTH)), - ('extra_data', social.apps.django_app.default.fields.JSONField( - default='{}')), - ('user', models.ForeignKey( - related_name='social_auth', to=USER_MODEL)), - ], - options={ - 'db_table': 'social_auth_usersocialauth', - }, - bases=(models.Model, social.storage.django_orm.DjangoUserMixin), - ), - migrations.AlterUniqueTogether( - name='usersocialauth', - unique_together={('provider', 'uid')}, - ), - migrations.AlterUniqueTogether( - name='code', - unique_together={('email', 'code')}, - ), - migrations.AlterUniqueTogether( - name='nonce', - unique_together={('server_url', 'timestamp', 'salt')}, - ), - ] diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py deleted file mode 100644 index 8a791eaea..000000000 --- a/social/apps/django_app/default/migrations/0002_add_related_name.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - -from social.utils import setting_name - -USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ - getattr(settings, 'AUTH_USER_MODEL', None) or \ - 'auth.User' - - -class Migration(migrations.Migration): - replaces = [('default', '0002_add_related_name')] - - dependencies = [ - ('social_auth', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='usersocialauth', - name='user', - field=models.ForeignKey(related_name='social_auth', to=USER_MODEL) - ), - ] diff --git a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py deleted file mode 100644 index 3557d707e..000000000 --- a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.conf import settings -from django.db import models, migrations - -from social.utils import setting_name - -EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254) - - -class Migration(migrations.Migration): - replaces = [('default', '0003_alter_email_max_length')] - - dependencies = [ - ('social_auth', '0002_add_related_name'), - ] - - operations = [ - migrations.AlterField( - model_name='code', - name='email', - field=models.EmailField(max_length=EMAIL_LENGTH), - ), - ] diff --git a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py deleted file mode 100644 index 82648bcb5..000000000 --- a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import social.apps.django_app.default.fields - - -class Migration(migrations.Migration): - replaces = [('default', '0004_auto_20160423_0400')] - - dependencies = [ - ('social_auth', '0003_alter_email_max_length'), - ] - - operations = [ - migrations.AlterField( - model_name='usersocialauth', - name='extra_data', - field=social.apps.django_app.default.fields.JSONField(default={}), - ), - ] diff --git a/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py b/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py deleted file mode 100644 index 3df56ef1c..000000000 --- a/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-07-28 02:33 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('social_auth', '0004_auto_20160423_0400'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='association', - unique_together=set([('server_url', 'handle')]), - ), - ] diff --git a/social/apps/django_app/default/migrations/__init__.py b/social/apps/django_app/default/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py index 047da9177..571423a8b 100644 --- a/social/apps/django_app/default/models.py +++ b/social/apps/django_app/default/models.py @@ -1,122 +1 @@ -"""Django ORM models for Social Auth""" -import six - -from django.db import models -from django.conf import settings -from django.db.utils import IntegrityError - -from social.utils import setting_name -from social.storage.django_orm import DjangoUserMixin, \ - DjangoAssociationMixin, \ - DjangoNonceMixin, \ - DjangoCodeMixin, \ - BaseDjangoStorage -from social.apps.django_app.default.fields import JSONField -from social.apps.django_app.default.managers import UserSocialAuthManager - - -USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \ - getattr(settings, 'AUTH_USER_MODEL', None) or \ - 'auth.User' -UID_LENGTH = getattr(settings, setting_name('UID_LENGTH'), 255) -EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254) -NONCE_SERVER_URL_LENGTH = getattr( - settings, setting_name('NONCE_SERVER_URL_LENGTH'), 255) -ASSOCIATION_SERVER_URL_LENGTH = getattr( - settings, setting_name('ASSOCIATION_SERVER_URL_LENGTH'), 255) -ASSOCIATION_HANDLE_LENGTH = getattr( - settings, setting_name('ASSOCIATION_HANDLE_LENGTH'), 255) - - -class AbstractUserSocialAuth(models.Model, DjangoUserMixin): - """Abstract Social Auth association model""" - user = models.ForeignKey(USER_MODEL, related_name='social_auth') - provider = models.CharField(max_length=32) - uid = models.CharField(max_length=UID_LENGTH) - extra_data = JSONField() - objects = UserSocialAuthManager() - - def __str__(self): - return str(self.user) - - class Meta: - abstract = True - - @classmethod - def get_social_auth(cls, provider, uid): - try: - return cls.objects.select_related('user').get(provider=provider, - uid=uid) - except UserSocialAuth.DoesNotExist: - return None - - @classmethod - def username_max_length(cls): - username_field = cls.username_field() - field = UserSocialAuth.user_model()._meta.get_field(username_field) - return field.max_length - - @classmethod - def user_model(cls): - user_model = UserSocialAuth._meta.get_field('user').rel.to - if isinstance(user_model, six.string_types): - app_label, model_name = user_model.split('.') - return models.get_model(app_label, model_name) - return user_model - - -class UserSocialAuth(AbstractUserSocialAuth): - """Social Auth association model""" - - class Meta: - """Meta data""" - unique_together = ('provider', 'uid') - db_table = 'social_auth_usersocialauth' - - -class Nonce(models.Model, DjangoNonceMixin): - """One use numbers""" - server_url = models.CharField(max_length=NONCE_SERVER_URL_LENGTH) - timestamp = models.IntegerField() - salt = models.CharField(max_length=65) - - class Meta: - unique_together = ('server_url', 'timestamp', 'salt') - db_table = 'social_auth_nonce' - - -class Association(models.Model, DjangoAssociationMixin): - """OpenId account association""" - server_url = models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH) - handle = models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH) - secret = models.CharField(max_length=255) # Stored base64 encoded - issued = models.IntegerField() - lifetime = models.IntegerField() - assoc_type = models.CharField(max_length=64) - - class Meta: - db_table = 'social_auth_association' - unique_together = ( - ('server_url', 'handle',) - ) - - -class Code(models.Model, DjangoCodeMixin): - email = models.EmailField(max_length=EMAIL_LENGTH) - code = models.CharField(max_length=32, db_index=True) - verified = models.BooleanField(default=False) - - class Meta: - db_table = 'social_auth_code' - unique_together = ('email', 'code') - - -class DjangoStorage(BaseDjangoStorage): - user = UserSocialAuth - nonce = Nonce - association = Association - code = Code - - @classmethod - def is_integrity_error(cls, exception): - return exception.__class__ is IntegrityError +from social_django.models import AbstractUserSocialAuth, UserSocialAuth, Nonce, Association, Code, DjangoStorage diff --git a/social/apps/django_app/default/tests.py b/social/apps/django_app/default/tests.py index db1bc1d7c..e69de29bb 100644 --- a/social/apps/django_app/default/tests.py +++ b/social/apps/django_app/default/tests.py @@ -1 +0,0 @@ -from social.apps.django_app.tests import * diff --git a/social/apps/django_app/me/__init__.py b/social/apps/django_app/me/__init__.py index 9bc91e231..e69de29bb 100644 --- a/social/apps/django_app/me/__init__.py +++ b/social/apps/django_app/me/__init__.py @@ -1,9 +0,0 @@ -""" -Mongoengine backend support. - -To enable this app: - * Add 'social.apps.django_app.me' to INSTALLED_APPS - * In urls.py include url('', include('social.apps.django_app.urls')) -""" -default_app_config = \ - 'social.apps.django_app.me.config.PythonSocialAuthConfig' diff --git a/social/apps/django_app/me/config.py b/social/apps/django_app/me/config.py index 1ece281f4..71e0d0cba 100644 --- a/social/apps/django_app/me/config.py +++ b/social/apps/django_app/me/config.py @@ -1,14 +1 @@ -from django.apps import AppConfig - - -class PythonSocialAuthConfig(AppConfig): - name = 'social.apps.django_app.me' - verbose_name = 'Python Social Auth' - - def ready(self): - from social.strategies.utils import set_current_strategy_getter - from social.apps.django_app.utils import load_strategy - # Set strategy loader method to workaround current strategy getter - # needed on get_user() method on authentication backends when working - # with Django - set_current_strategy_getter(load_strategy) +from social_django_mongoengine.config import PythonSocialAuthConfig diff --git a/social/apps/django_app/me/models.py b/social/apps/django_app/me/models.py index 94770343b..5ea5a3b98 100644 --- a/social/apps/django_app/me/models.py +++ b/social/apps/django_app/me/models.py @@ -1,77 +1 @@ -""" -MongoEngine Django models for Social Auth. -Requires MongoEngine 0.8.6 or higher. -""" -from django.conf import settings - -from mongoengine import Document, ReferenceField -from mongoengine.queryset import OperationError - -from social.utils import setting_name, module_member -from social.storage.django_orm import BaseDjangoStorage - -from social.storage.mongoengine_orm import MongoengineUserMixin, \ - MongoengineNonceMixin, \ - MongoengineAssociationMixin, \ - MongoengineCodeMixin - - -UNUSABLE_PASSWORD = '!' # Borrowed from django 1.4 - - -def _get_user_model(): - """ - Get the User Document class user for MongoEngine authentication. - - Use the model defined in SOCIAL_AUTH_USER_MODEL if defined, or - defaults to MongoEngine's configured user document class. - """ - custom_model = getattr(settings, setting_name('USER_MODEL'), None) - if custom_model: - return module_member(custom_model) - - try: - # Custom user model support with MongoEngine 0.8 - from mongoengine.django.mongo_auth.models import get_user_document - return get_user_document() - except ImportError: - return module_member('mongoengine.django.auth.User') - - -USER_MODEL = _get_user_model() - - -class UserSocialAuth(Document, MongoengineUserMixin): - """Social Auth association model""" - user = ReferenceField(USER_MODEL) - - @classmethod - def user_model(cls): - return USER_MODEL - - -class Nonce(Document, MongoengineNonceMixin): - """One use numbers""" - pass - - -class Association(Document, MongoengineAssociationMixin): - """OpenId account association""" - pass - - -class Code(Document, MongoengineCodeMixin): - """Mail validation single one time use code""" - pass - - -class DjangoStorage(BaseDjangoStorage): - user = UserSocialAuth - nonce = Nonce - association = Association - code = Code - - @classmethod - def is_integrity_error(cls, exception): - return exception.__class__ is OperationError and \ - 'E11000' in exception.message +from social_django_mongoengine.models import _get_user_model, UserSocialAuth, Nonce, Association, Code, DjangoStorage diff --git a/social/apps/django_app/me/tests.py b/social/apps/django_app/me/tests.py index db1bc1d7c..e69de29bb 100644 --- a/social/apps/django_app/me/tests.py +++ b/social/apps/django_app/me/tests.py @@ -1 +0,0 @@ -from social.apps.django_app.tests import * diff --git a/social/apps/django_app/middleware.py b/social/apps/django_app/middleware.py index fefa6b230..4335812c7 100644 --- a/social/apps/django_app/middleware.py +++ b/social/apps/django_app/middleware.py @@ -1,62 +1 @@ -# -*- coding: utf-8 -*- -import six - -from django.conf import settings -from django.contrib import messages -from django.contrib.messages.api import MessageFailure -from django.shortcuts import redirect -from django.utils.http import urlquote - -from social.exceptions import SocialAuthBaseException -from social.utils import social_logger - -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - MiddlewareMixin = object - - -class SocialAuthExceptionMiddleware(MiddlewareMixin): - """Middleware that handles Social Auth AuthExceptions by providing the user - with a message, logging an error, and redirecting to some next location. - - By default, the exception message itself is sent to the user and they are - redirected to the location specified in the SOCIAL_AUTH_LOGIN_ERROR_URL - setting. - - This middleware can be extended by overriding the get_message or - get_redirect_uri methods, which each accept request and exception. - """ - def process_exception(self, request, exception): - strategy = getattr(request, 'social_strategy', None) - if strategy is None or self.raise_exception(request, exception): - return - - if isinstance(exception, SocialAuthBaseException): - backend = getattr(request, 'backend', None) - backend_name = getattr(backend, 'name', 'unknown-backend') - - message = self.get_message(request, exception) - social_logger.error(message) - - url = self.get_redirect_uri(request, exception) - try: - messages.error(request, message, - extra_tags='social-auth ' + backend_name) - except MessageFailure: - url += ('?' in url and '&' or '?') + \ - 'message={0}&backend={1}'.format(urlquote(message), - backend_name) - return redirect(url) - - def raise_exception(self, request, exception): - strategy = getattr(request, 'social_strategy', None) - if strategy is not None: - return strategy.setting('RAISE_EXCEPTIONS', settings.DEBUG) - - def get_message(self, request, exception): - return six.text_type(exception) - - def get_redirect_uri(self, request, exception): - strategy = getattr(request, 'social_strategy', None) - return strategy.setting('LOGIN_ERROR_URL') +from social_django.middleware import SocialAuthExceptionMiddleware diff --git a/social/apps/django_app/tests.py b/social/apps/django_app/tests.py index 1dec95ef7..e69de29bb 100644 --- a/social/apps/django_app/tests.py +++ b/social/apps/django_app/tests.py @@ -1,51 +0,0 @@ -from social.tests.test_exceptions import * -from social.tests.test_pipeline import * -from social.tests.test_storage import * -from social.tests.test_utils import * -from social.tests.actions.test_associate import * -from social.tests.actions.test_disconnect import * -from social.tests.actions.test_login import * -from social.tests.backends.test_amazon import * -from social.tests.backends.test_angel import * -from social.tests.backends.test_behance import * -from social.tests.backends.test_bitbucket import * -from social.tests.backends.test_box import * -from social.tests.backends.test_broken import * -from social.tests.backends.test_coinbase import * -from social.tests.backends.test_dailymotion import * -from social.tests.backends.test_disqus import * -from social.tests.backends.test_dropbox import * -from social.tests.backends.test_dummy import * -from social.tests.backends.test_email import * -from social.tests.backends.test_evernote import * -from social.tests.backends.test_facebook import * -from social.tests.backends.test_fitbit import * -from social.tests.backends.test_flickr import * -from social.tests.backends.test_foursquare import * -from social.tests.backends.test_google import * -from social.tests.backends.test_instagram import * -from social.tests.backends.test_linkedin import * -from social.tests.backends.test_live import * -from social.tests.backends.test_livejournal import * -from social.tests.backends.test_mixcloud import * -from social.tests.backends.test_podio import * -from social.tests.backends.test_readability import * -from social.tests.backends.test_reddit import * -from social.tests.backends.test_sketchfab import * -from social.tests.backends.test_skyrock import * -from social.tests.backends.test_soundcloud import * -from social.tests.backends.test_stackoverflow import * -from social.tests.backends.test_steam import * -from social.tests.backends.test_stocktwits import * -from social.tests.backends.test_stripe import * -from social.tests.backends.test_thisismyjam import * -from social.tests.backends.test_tripit import * -from social.tests.backends.test_tumblr import * -from social.tests.backends.test_twitter import * -from social.tests.backends.test_username import * -from social.tests.backends.test_utils import * -from social.tests.backends.test_vk import * -from social.tests.backends.test_xing import * -from social.tests.backends.test_yahoo import * -from social.tests.backends.test_yammer import * -from social.tests.backends.test_yandex import * diff --git a/social/apps/django_app/urls.py b/social/apps/django_app/urls.py index fc85107fb..38ed3833e 100644 --- a/social/apps/django_app/urls.py +++ b/social/apps/django_app/urls.py @@ -1,26 +1 @@ -"""URLs module""" -from django.conf import settings -try: - from django.conf.urls import url -except ImportError: - # Django < 1.4 - from django.conf.urls.defaults import url - -from social.utils import setting_name -from social.apps.django_app import views - - -extra = getattr(settings, setting_name('TRAILING_SLASH'), True) and '/' or '' - -urlpatterns = [ - # authentication / association - url(r'^login/(?P[^/]+){0}$'.format(extra), views.auth, - name='begin'), - url(r'^complete/(?P[^/]+){0}$'.format(extra), views.complete, - name='complete'), - # disconnection - url(r'^disconnect/(?P[^/]+){0}$'.format(extra), views.disconnect, - name='disconnect'), - url(r'^disconnect/(?P[^/]+)/(?P[^/]+){0}$' - .format(extra), views.disconnect, name='disconnect_individual'), -] +from social_django.urls import urlpatterns, app_name diff --git a/social/apps/django_app/utils.py b/social/apps/django_app/utils.py index b4a50ec5e..14d7d8b1e 100644 --- a/social/apps/django_app/utils.py +++ b/social/apps/django_app/utils.py @@ -1,74 +1 @@ -import warnings - -from functools import wraps - -from django.conf import settings -from django.core.urlresolvers import reverse -from django.http import Http404 - -from social.utils import setting_name, module_member -from social.exceptions import MissingBackend -from social.strategies.utils import get_strategy -from social.backends.utils import get_backend - - -BACKENDS = settings.AUTHENTICATION_BACKENDS -STRATEGY = getattr(settings, setting_name('STRATEGY'), - 'social.strategies.django_strategy.DjangoStrategy') -STORAGE = getattr(settings, setting_name('STORAGE'), - 'social.apps.django_app.default.models.DjangoStorage') -Strategy = module_member(STRATEGY) -Storage = module_member(STORAGE) - - -def load_strategy(request=None): - return get_strategy(STRATEGY, STORAGE, request) - - -def load_backend(strategy, name, redirect_uri): - Backend = get_backend(BACKENDS, name) - return Backend(strategy, redirect_uri) - - -def psa(redirect_uri=None, load_strategy=load_strategy): - def decorator(func): - @wraps(func) - def wrapper(request, backend, *args, **kwargs): - uri = redirect_uri - if uri and not uri.startswith('/'): - uri = reverse(redirect_uri, args=(backend,)) - request.social_strategy = load_strategy(request) - # backward compatibility in attribute name, only if not already - # defined - if not hasattr(request, 'strategy'): - request.strategy = request.social_strategy - - try: - request.backend = load_backend(request.social_strategy, - backend, uri) - except MissingBackend: - raise Http404('Backend not found') - return func(request, backend, *args, **kwargs) - return wrapper - return decorator - - -def setting(name, default=None): - try: - return getattr(settings, setting_name(name)) - except AttributeError: - return getattr(settings, name, default) - - -class BackendWrapper(object): - # XXX: Deprecated, restored to avoid session issues - def authenticate(self, *args, **kwargs): - return None - - def get_user(self, user_id): - return Strategy(storage=Storage).get_user(user_id) - - -def strategy(*args, **kwargs): - warnings.warn('@strategy decorator is deprecated, use @psa instead') - return psa(*args, **kwargs) +from social_django.utils import load_strategy, load_backend, psa, setting, BackendWrapper, strategy diff --git a/social/apps/django_app/views.py b/social/apps/django_app/views.py index 5ba1d59a0..9b93aeeb1 100644 --- a/social/apps/django_app/views.py +++ b/social/apps/django_app/views.py @@ -1,59 +1 @@ -from django.conf import settings -from django.contrib.auth import login, REDIRECT_FIELD_NAME -from django.contrib.auth.decorators import login_required -from django.views.decorators.csrf import csrf_exempt, csrf_protect -from django.views.decorators.http import require_POST -from django.views.decorators.cache import never_cache - -from social.utils import setting_name -from social.actions import do_auth, do_complete, do_disconnect -from social.apps.django_app.utils import psa - - -NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'social' - - -@never_cache -@psa('{0}:complete'.format(NAMESPACE)) -def auth(request, backend): - return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME) - - -@never_cache -@csrf_exempt -@psa('{0}:complete'.format(NAMESPACE)) -def complete(request, backend, *args, **kwargs): - """Authentication complete view""" - return do_complete(request.backend, _do_login, request.user, - redirect_name=REDIRECT_FIELD_NAME, *args, **kwargs) - - -@never_cache -@login_required -@psa() -@require_POST -@csrf_protect -def disconnect(request, backend, association_id=None): - """Disconnects given backend from current logged in user.""" - return do_disconnect(request.backend, request.user, association_id, - redirect_name=REDIRECT_FIELD_NAME) - - -def _do_login(backend, user, social_user): - user.backend = '{0}.{1}'.format(backend.__module__, - backend.__class__.__name__) - login(backend.strategy.request, user) - if backend.setting('SESSION_EXPIRATION', False): - # Set session expiration date if present and enabled - # by setting. Use last social-auth instance for current - # provider, users can associate several accounts with - # a same provider. - expiration = social_user.expiration_datetime() - if expiration: - try: - backend.strategy.request.session.set_expiry( - expiration.seconds + expiration.days * 86400 - ) - except OverflowError: - # Handle django time zone overflow - backend.strategy.request.session.set_expiry(None) +from social_django.views import auth, complete, disconnect, _do_login diff --git a/social/apps/flask_app/__init__.py b/social/apps/flask_app/__init__.py index e98cdc1cc..e69de29bb 100644 --- a/social/apps/flask_app/__init__.py +++ b/social/apps/flask_app/__init__.py @@ -1,5 +0,0 @@ -from social.strategies.utils import set_current_strategy_getter -from social.apps.flask_app.utils import load_strategy - - -set_current_strategy_getter(load_strategy) diff --git a/social/apps/flask_app/default/models.py b/social/apps/flask_app/default/models.py index 556adc1b9..1913b8951 100644 --- a/social/apps/flask_app/default/models.py +++ b/social/apps/flask_app/default/models.py @@ -1,76 +1,2 @@ -"""Flask SQLAlchemy ORM models for Social Auth""" -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.orm import relationship, backref -from sqlalchemy.schema import UniqueConstraint -from sqlalchemy.ext.declarative import declarative_base - -from social.utils import setting_name, module_member -from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ - SQLAlchemyAssociationMixin, \ - SQLAlchemyNonceMixin, \ - SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage - - -PSABase = declarative_base() - - -class _AppSession(PSABase): - __abstract__ = True - - @classmethod - def _set_session(cls, app_session): - cls.app_session = app_session - - @classmethod - def _session(cls): - return cls.app_session - - -class UserSocialAuth(_AppSession, SQLAlchemyUserMixin): - """Social Auth association model""" - # Temporary override of constraints to avoid an error on the still-to-be - # missing column uid. - __table_args__ = () - - @classmethod - def user_model(cls): - return cls.user.property.argument - - @classmethod - def username_max_length(cls): - user_model = cls.user_model() - return user_model.__table__.columns.get('username').type.length - - -class Nonce(_AppSession, SQLAlchemyNonceMixin): - """One use numbers""" - pass - - -class Association(_AppSession, SQLAlchemyAssociationMixin): - """OpenId account association""" - pass - - -class Code(_AppSession, SQLAlchemyCodeMixin): - pass - - -class FlaskStorage(BaseSQLAlchemyStorage): - user = UserSocialAuth - nonce = Nonce - association = Association - code = Code - - -def init_social(app, session): - UID_LENGTH = app.config.get(setting_name('UID_LENGTH'), 255) - User = module_member(app.config[setting_name('USER_MODEL')]) - _AppSession._set_session(session) - UserSocialAuth.__table_args__ = (UniqueConstraint('provider', 'uid'),) - UserSocialAuth.uid = Column(String(UID_LENGTH)) - UserSocialAuth.user_id = Column(Integer, ForeignKey(User.id), - nullable=False, index=True) - UserSocialAuth.user = relationship(User, backref=backref('social_auth', - lazy='dynamic')) +from social_flask.models import PSABase, _AppSession, UserSocialAuth, Nonce, \ + Association, Code, FlaskStorage, init_social diff --git a/social/apps/flask_app/me/models.py b/social/apps/flask_app/me/models.py index fe91d62e2..16b626b0b 100644 --- a/social/apps/flask_app/me/models.py +++ b/social/apps/flask_app/me/models.py @@ -1,45 +1 @@ -"""Flask SQLAlchemy ORM models for Social Auth""" -from mongoengine import ReferenceField - -from social.utils import setting_name, module_member -from social.storage.mongoengine_orm import MongoengineUserMixin, \ - MongoengineAssociationMixin, \ - MongoengineNonceMixin, \ - MongoengineCodeMixin, \ - BaseMongoengineStorage - - -class FlaskStorage(BaseMongoengineStorage): - user = None - nonce = None - association = None - code = None - - -def init_social(app, db): - User = module_member(app.config[setting_name('USER_MODEL')]) - - class UserSocialAuth(db.Document, MongoengineUserMixin): - """Social Auth association model""" - user = ReferenceField(User) - - @classmethod - def user_model(cls): - return User - - class Nonce(db.Document, MongoengineNonceMixin): - """One use numbers""" - pass - - class Association(db.Document, MongoengineAssociationMixin): - """OpenId account association""" - pass - - class Code(db.Document, MongoengineCodeMixin): - pass - - # Set the references in the storage class - FlaskStorage.user = UserSocialAuth - FlaskStorage.nonce = Nonce - FlaskStorage.association = Association - FlaskStorage.code = Code +from social_flask_mongoengine.models import FlaskStorage, init_social diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py index 497e9a5a4..cb4fad670 100644 --- a/social/apps/flask_app/peewee/models.py +++ b/social/apps/flask_app/peewee/models.py @@ -1,48 +1 @@ -"""Flask Peewee ORM models for Social Auth""" -from peewee import Model, ForeignKeyField, Proxy - -from social.utils import setting_name, module_member -from social.storage.peewee_orm import PeeweeUserMixin, \ - PeeweeAssociationMixin, \ - PeeweeNonceMixin, \ - PeeweeCodeMixin, \ - BasePeeweeStorage, \ - database_proxy - - -class FlaskStorage(BasePeeweeStorage): - user = None - nonce = None - association = None - code = None - - -def init_social(app, db): - User = module_member(app.config[setting_name('USER_MODEL')]) - - database_proxy.initialize(db) - - class UserSocialAuth(PeeweeUserMixin): - """Social Auth association model""" - user = ForeignKeyField(User, related_name='social_auth') - - @classmethod - def user_model(cls): - return User - - class Nonce(PeeweeNonceMixin): - """One use numbers""" - pass - - class Association(PeeweeAssociationMixin): - """OpenId account association""" - pass - - class Code(PeeweeCodeMixin): - pass - - # Set the references in the storage class - FlaskStorage.user = UserSocialAuth - FlaskStorage.nonce = Nonce - FlaskStorage.association = Association - FlaskStorage.code = Code +fro social_flask_peewee.models import FlaskStorage, init_social diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py index b3b140631..989b50944 100644 --- a/social/apps/flask_app/routes.py +++ b/social/apps/flask_app/routes.py @@ -1,45 +1 @@ -from flask import g, Blueprint, request -from flask_login import login_required, login_user - -from social.actions import do_auth, do_complete, do_disconnect -from social.apps.flask_app.utils import psa - - -social_auth = Blueprint('social', __name__) - - -@social_auth.route('/login//', methods=('GET', 'POST')) -@psa('social.complete') -def auth(backend): - return do_auth(g.backend) - - -@social_auth.route('/complete//', methods=('GET', 'POST')) -@psa('social.complete') -def complete(backend, *args, **kwargs): - """Authentication complete view, override this view if transaction - management doesn't suit your needs.""" - return do_complete(g.backend, login=do_login, user=g.user, - *args, **kwargs) - - -@social_auth.route('/disconnect//', methods=('POST',)) -@social_auth.route('/disconnect///', - methods=('POST',)) -@social_auth.route('/disconnect///', - methods=('POST',)) -@login_required -@psa() -def disconnect(backend, association_id=None): - """Disconnects given backend from current logged in user.""" - return do_disconnect(g.backend, g.user, association_id) - - -def do_login(backend, user, social_user): - name = backend.strategy.setting('REMEMBER_SESSION_NAME', 'keep') - remember = backend.strategy.session_get(name) or \ - request.cookies.get(name) or \ - request.args.get(name) or \ - request.form.get(name) or \ - False - return login_user(user, remember=remember) +from social_flask.routes import auth, complete, disconnect, do_login diff --git a/social/apps/flask_app/template_filters.py b/social/apps/flask_app/template_filters.py index 341029a03..1e75a5cfc 100644 --- a/social/apps/flask_app/template_filters.py +++ b/social/apps/flask_app/template_filters.py @@ -1,25 +1 @@ -from flask import g, request - -from social.backends.utils import user_backends_data -from social.apps.flask_app.utils import get_helper - - -def backends(): - """Load Social Auth current user data to context under the key 'backends'. - Will return the output of social.backends.utils.user_backends_data.""" - return { - 'backends': user_backends_data(g.user, - get_helper('AUTHENTICATION_BACKENDS'), - get_helper('STORAGE', do_import=True)) - } - - -def login_redirect(): - """Load current redirect to context.""" - value = request.form.get('next', '') or \ - request.args.get('next', '') - return { - 'REDIRECT_FIELD_NAME': 'next', - 'REDIRECT_FIELD_VALUE': value, - 'REDIRECT_QUERYSTRING': value and ('next=' + value) or '' - } +from social_flask.template_filters import backends, login_redirect diff --git a/social/apps/flask_app/utils.py b/social/apps/flask_app/utils.py index 2e77bcaf6..f80ca7093 100644 --- a/social/apps/flask_app/utils.py +++ b/social/apps/flask_app/utils.py @@ -1,53 +1 @@ -import warnings - -from functools import wraps - -from flask import current_app, url_for, g - -from social.utils import module_member, setting_name -from social.strategies.utils import get_strategy -from social.backends.utils import get_backend - - -DEFAULTS = { - 'STORAGE': 'social.apps.flask_app.default.models.FlaskStorage', - 'STRATEGY': 'social.strategies.flask_strategy.FlaskStrategy' -} - - -def get_helper(name, do_import=False): - config = current_app.config.get(setting_name(name), - DEFAULTS.get(name, None)) - return do_import and module_member(config) or config - - -def load_strategy(): - strategy = get_helper('STRATEGY') - storage = get_helper('STORAGE') - return get_strategy(strategy, storage) - - -def load_backend(strategy, name, redirect_uri, *args, **kwargs): - backends = get_helper('AUTHENTICATION_BACKENDS') - Backend = get_backend(backends, name) - return Backend(strategy=strategy, redirect_uri=redirect_uri) - - -def psa(redirect_uri=None): - def decorator(func): - @wraps(func) - def wrapper(backend, *args, **kwargs): - uri = redirect_uri - if uri and not uri.startswith('/'): - uri = url_for(uri, backend=backend) - g.strategy = load_strategy() - g.backend = load_backend(g.strategy, backend, redirect_uri=uri, - *args, **kwargs) - return func(backend, *args, **kwargs) - return wrapper - return decorator - - -def strategy(*args, **kwargs): - warnings.warn('@strategy decorator is deprecated, use @psa instead') - return psa(*args, **kwargs) +from social_flask.utils import get_helper, load_strategy, load_backend, psa, strategy diff --git a/social/apps/pyramid_app/__init__.py b/social/apps/pyramid_app/__init__.py index 4ea333f87..0ce0ef196 100644 --- a/social/apps/pyramid_app/__init__.py +++ b/social/apps/pyramid_app/__init__.py @@ -1,13 +1 @@ -from social.strategies.utils import set_current_strategy_getter -from social.apps.pyramid_app.utils import load_strategy - - -def includeme(config): - config.add_route('social.auth', '/login/{backend}') - config.add_route('social.complete', '/complete/{backend}') - config.add_route('social.disconnect', '/disconnect/{backend}') - config.add_route('social.disconnect_association', - '/disconnect/{backend}/{association_id}') - - -set_current_strategy_getter(load_strategy) +from social_pyramid.__init__ import includeme diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py index 5752f3f51..921638454 100644 --- a/social/apps/pyramid_app/models.py +++ b/social/apps/pyramid_app/models.py @@ -1,64 +1 @@ -"""Pyramid SQLAlchemy ORM models for Social Auth""" -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.orm import relationship, backref - -from social.utils import setting_name, module_member -from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ - SQLAlchemyAssociationMixin, \ - SQLAlchemyNonceMixin, \ - SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage - - -class PyramidStorage(BaseSQLAlchemyStorage): - user = None - nonce = None - association = None - - -def init_social(config, Base, session): - if hasattr(config, 'registry'): - config = config.registry.settings - UID_LENGTH = config.get(setting_name('UID_LENGTH'), 255) - User = module_member(config[setting_name('USER_MODEL')]) - app_session = session - - class _AppSession(object): - COMMIT_SESSION = False - - @classmethod - def _session(cls): - return app_session - - class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin): - """Social Auth association model""" - uid = Column(String(UID_LENGTH)) - user_id = Column(User.id.type, ForeignKey(User.id), - nullable=False, index=True) - user = relationship(User, backref=backref('social_auth', - lazy='dynamic')) - - @classmethod - def username_max_length(cls): - return User.__table__.columns.get('username').type.length - - @classmethod - def user_model(cls): - return User - - class Nonce(_AppSession, Base, SQLAlchemyNonceMixin): - """One use numbers""" - pass - - class Association(_AppSession, Base, SQLAlchemyAssociationMixin): - """OpenId account association""" - pass - - class Code(_AppSession, Base, SQLAlchemyCodeMixin): - pass - - # Set the references in the storage class - PyramidStorage.user = UserSocialAuth - PyramidStorage.nonce = Nonce - PyramidStorage.association = Association - PyramidStorage.code = Code +from social_pyramid.models import PyramidStorage, init_social diff --git a/social/apps/pyramid_app/utils.py b/social/apps/pyramid_app/utils.py index d0c3adde1..14b93b438 100644 --- a/social/apps/pyramid_app/utils.py +++ b/social/apps/pyramid_app/utils.py @@ -1,82 +1 @@ -import warnings - -from functools import wraps - -from pyramid.threadlocal import get_current_registry -from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden - -from social.utils import setting_name, module_member -from social.strategies.utils import get_strategy -from social.backends.utils import get_backend, user_backends_data - - -DEFAULTS = { - 'STORAGE': 'social.apps.pyramid_app.models.PyramidStorage', - 'STRATEGY': 'social.strategies.pyramid_strategy.PyramidStrategy' -} - - -def get_helper(name): - settings = get_current_registry().settings - return settings.get(setting_name(name), DEFAULTS.get(name, None)) - - -def load_strategy(request): - return get_strategy( - get_helper('STRATEGY'), - get_helper('STORAGE'), - request - ) - - -def load_backend(strategy, name, redirect_uri): - backends = get_helper('AUTHENTICATION_BACKENDS') - Backend = get_backend(backends, name) - return Backend(strategy=strategy, redirect_uri=redirect_uri) - - -def psa(redirect_uri=None): - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - backend = request.matchdict.get('backend') - if not backend: - return HTTPNotFound('Missing backend') - - uri = redirect_uri - if uri and not uri.startswith('/'): - uri = request.route_url(uri, backend=backend) - - request.strategy = load_strategy(request) - request.backend = load_backend(request.strategy, backend, uri) - return func(request, *args, **kwargs) - return wrapper - return decorator - - -def login_required(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - is_logged_in = module_member( - request.backend.setting('LOGGEDIN_FUNCTION') - ) - if not is_logged_in(request): - raise HTTPForbidden('Not authorized user') - return func(request, *args, **kwargs) - return wrapper - - -def backends(request, user): - """Load Social Auth current user data to context under the key 'backends'. - Will return the output of social.backends.utils.user_backends_data.""" - storage = module_member(get_helper('STORAGE')) - return { - 'backends': user_backends_data( - user, get_helper('AUTHENTICATION_BACKENDS'), storage - ) - } - - -def strategy(*args, **kwargs): - warnings.warn('@strategy decorator is deprecated, use @psa instead') - return psa(*args, **kwargs) +from social_pyramid.utils import get_helper, load_strategy, load_backend, psa, login_required, backends, strategy diff --git a/social/apps/pyramid_app/views.py b/social/apps/pyramid_app/views.py index 7587ff929..3dad86e82 100644 --- a/social/apps/pyramid_app/views.py +++ b/social/apps/pyramid_app/views.py @@ -1,30 +1 @@ -from pyramid.view import view_config - -from social.utils import module_member -from social.actions import do_auth, do_complete, do_disconnect -from social.apps.pyramid_app.utils import psa, login_required - - -@view_config(route_name='social.auth', request_method=('GET', 'POST')) -@psa('social.complete') -def auth(request): - return do_auth(request.backend, redirect_name='next') - - -@view_config(route_name='social.complete', request_method=('GET', 'POST')) -@psa('social.complete') -def complete(request, *args, **kwargs): - do_login = module_member(request.backend.setting('LOGIN_FUNCTION')) - return do_complete(request.backend, do_login, request.user, - redirect_name='next', *args, **kwargs) - - -@view_config(route_name='social.disconnect', request_method=('POST',)) -@view_config(route_name='social.disconnect_association', - request_method=('POST',)) -@psa() -@login_required -def disconnect(request): - return do_disconnect(request.backend, request.user, - request.matchdict.get('association_id'), - redirect_name='next') +from social_pyramid.views import auth, complete, disconnect diff --git a/social/apps/tornado_app/handlers.py b/social/apps/tornado_app/handlers.py index f31869692..362ddac70 100644 --- a/social/apps/tornado_app/handlers.py +++ b/social/apps/tornado_app/handlers.py @@ -1,50 +1 @@ -from tornado.web import RequestHandler - -from social.apps.tornado_app.utils import psa -from social.actions import do_auth, do_complete, do_disconnect - - -class BaseHandler(RequestHandler): - def user_id(self): - return self.get_secure_cookie('user_id') - - def get_current_user(self): - user_id = self.user_id() - if user_id: - return self.backend.strategy.get_user(int(user_id)) - - def login_user(self, user): - self.set_secure_cookie('user_id', str(user.id)) - - -class AuthHandler(BaseHandler): - def get(self, backend): - self._auth(backend) - - def post(self, backend): - self._auth(backend) - - @psa('complete') - def _auth(self, backend): - do_auth(self.backend) - - -class CompleteHandler(BaseHandler): - def get(self, backend): - self._complete(backend) - - def post(self, backend): - self._complete(backend) - - @psa('complete') - def _complete(self, backend): - do_complete( - self.backend, - login=lambda backend, user, social_user: self.login_user(user), - user=self.get_current_user() - ) - - -class DisconnectHandler(BaseHandler): - def post(self): - do_disconnect() +from social_tornado.handlers import BaseHandler, AuthHandler, CompleteHandler, DisconnectHandler diff --git a/social/apps/tornado_app/models.py b/social/apps/tornado_app/models.py index 803d24ed0..e3c48f084 100644 --- a/social/apps/tornado_app/models.py +++ b/social/apps/tornado_app/models.py @@ -1,61 +1 @@ -"""Tornado SQLAlchemy ORM models for Social Auth""" -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.orm import relationship, backref - -from social.utils import setting_name, module_member -from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ - SQLAlchemyAssociationMixin, \ - SQLAlchemyNonceMixin, \ - SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage - - -class TornadoStorage(BaseSQLAlchemyStorage): - user = None - nonce = None - association = None - code = None - - -def init_social(Base, session, settings): - UID_LENGTH = settings.get(setting_name('UID_LENGTH'), 255) - User = module_member(settings[setting_name('USER_MODEL')]) - app_session = session - - class _AppSession(object): - @classmethod - def _session(cls): - return app_session - - class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin): - """Social Auth association model""" - uid = Column(String(UID_LENGTH)) - user_id = Column(Integer, ForeignKey(User.id), - nullable=False, index=True) - user = relationship(User, backref=backref('social_auth', - lazy='dynamic')) - - @classmethod - def username_max_length(cls): - return User.__table__.columns.get('username').type.length - - @classmethod - def user_model(cls): - return User - - class Nonce(_AppSession, Base, SQLAlchemyNonceMixin): - """One use numbers""" - pass - - class Association(_AppSession, Base, SQLAlchemyAssociationMixin): - """OpenId account association""" - pass - - class Code(_AppSession, Base, SQLAlchemyCodeMixin): - pass - - # Set the references in the storage class - TornadoStorage.user = UserSocialAuth - TornadoStorage.nonce = Nonce - TornadoStorage.association = Association - TornadoStorage.code = Code +from social_tornado.models import TornadoStorage, init_social diff --git a/social/apps/tornado_app/routes.py b/social/apps/tornado_app/routes.py index b671f9feb..e69de29bb 100644 --- a/social/apps/tornado_app/routes.py +++ b/social/apps/tornado_app/routes.py @@ -1,13 +0,0 @@ -from tornado.web import url - -from .handlers import AuthHandler, CompleteHandler, DisconnectHandler - - -SOCIAL_AUTH_ROUTES = [ - url(r'/login/(?P[^/]+)/?', AuthHandler, name='begin'), - url(r'/complete/(?P[^/]+)/', CompleteHandler, name='complete'), - url(r'/disconnect/(?P[^/]+)/?', DisconnectHandler, - name='disconnect'), - url(r'/disconnect/(?P[^/]+)/(?P\d+)/?', - DisconnectHandler, name='disconect_individual'), -] diff --git a/social/apps/tornado_app/utils.py b/social/apps/tornado_app/utils.py index 04e4a13dc..3bf1efc77 100644 --- a/social/apps/tornado_app/utils.py +++ b/social/apps/tornado_app/utils.py @@ -1,49 +1 @@ -import warnings - -from functools import wraps - -from social.utils import setting_name -from social.strategies.utils import get_strategy -from social.backends.utils import get_backend - - -DEFAULTS = { - 'STORAGE': 'social.apps.tornado_app.models.TornadoStorage', - 'STRATEGY': 'social.strategies.tornado_strategy.TornadoStrategy' -} - - -def get_helper(request_handler, name): - return request_handler.settings.get(setting_name(name), - DEFAULTS.get(name, None)) - - -def load_strategy(request_handler): - strategy = get_helper(request_handler, 'STRATEGY') - storage = get_helper(request_handler, 'STORAGE') - return get_strategy(strategy, storage, request_handler) - - -def load_backend(request_handler, strategy, name, redirect_uri): - backends = get_helper(request_handler, 'AUTHENTICATION_BACKENDS') - Backend = get_backend(backends, name) - return Backend(strategy, redirect_uri) - - -def psa(redirect_uri=None): - def decorator(func): - @wraps(func) - def wrapper(self, backend, *args, **kwargs): - uri = redirect_uri - if uri and not uri.startswith('/'): - uri = self.reverse_url(uri, backend) - self.strategy = load_strategy(self) - self.backend = load_backend(self, self.strategy, backend, uri) - return func(self, backend, *args, **kwargs) - return wrapper - return decorator - - -def strategy(*args, **kwargs): - warnings.warn('@strategy decorator is deprecated, use @psa instead') - return psa(*args, **kwargs) +from social_tornado.utils import get_helper, load_strategy, load_backend, psa, strategy diff --git a/social/apps/webpy_app/__init__.py b/social/apps/webpy_app/__init__.py index 811d0f251..e69de29bb 100644 --- a/social/apps/webpy_app/__init__.py +++ b/social/apps/webpy_app/__init__.py @@ -1,5 +0,0 @@ -from social.strategies.utils import set_current_strategy_getter -from social.apps.webpy_app.utils import load_strategy - - -set_current_strategy_getter(load_strategy) diff --git a/social/apps/webpy_app/app.py b/social/apps/webpy_app/app.py index 3a78b642a..a5173a746 100644 --- a/social/apps/webpy_app/app.py +++ b/social/apps/webpy_app/app.py @@ -1,73 +1 @@ -import web - -from social.actions import do_auth, do_complete, do_disconnect -from social.apps.webpy_app.utils import psa, load_strategy - - -urls = ( - '/login/(?P[^/]+)/?', 'auth', - '/complete/(?P[^/]+)/?', 'complete', - '/disconnect/(?P[^/]+)/?', 'disconnect', - '/disconnect/(?P[^/]+)/(?P\d+)/?', 'disconnect', -) - - -class BaseViewClass(object): - def __init__(self, *args, **kwargs): - self.session = web.web_session - method = web.ctx.method == 'POST' and 'post' or 'get' - self.strategy = load_strategy() - self.data = web.input(_method=method) - super(BaseViewClass, self).__init__(*args, **kwargs) - - def get_current_user(self): - if not hasattr(self, '_user'): - if self.session.get('logged_in'): - self._user = self.strategy.get_user( - self.session.get('user_id') - ) - else: - self._user = None - return self._user - - def login_user(self, user): - self.session['logged_in'] = True - self.session['user_id'] = user.id - - -class auth(BaseViewClass): - def GET(self, backend): - return self._auth(backend) - - def POST(self, backend): - return self._auth(backend) - - @psa('/complete/%(backend)s/') - def _auth(self, backend): - return do_auth(self.backend) - - -class complete(BaseViewClass): - def GET(self, backend, *args, **kwargs): - return self._complete(backend, *args, **kwargs) - - def POST(self, backend, *args, **kwargs): - return self._complete(backend, *args, **kwargs) - - @psa('/complete/%(backend)s/') - def _complete(self, backend, *args, **kwargs): - return do_complete( - self.backend, - login=lambda backend, user, social_user: self.login_user(user), - user=self.get_current_user(), *args, **kwargs - ) - - -class disconnect(BaseViewClass): - @psa() - def POST(self, backend, association_id=None): - return do_disconnect(self.backend, self.get_current_user(), - association_id) - - -app_social = web.application(urls, locals()) +from social_webpy.app import BaseViewClass, auth, complete, disconnect diff --git a/social/apps/webpy_app/models.py b/social/apps/webpy_app/models.py index 68b91f720..0ba372c82 100644 --- a/social/apps/webpy_app/models.py +++ b/social/apps/webpy_app/models.py @@ -1,62 +1 @@ -"""Flask SQLAlchemy ORM models for Social Auth""" -import web - -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.orm import relationship -from sqlalchemy.ext.declarative import declarative_base - -from social.utils import setting_name, module_member -from social.storage.sqlalchemy_orm import SQLAlchemyUserMixin, \ - SQLAlchemyAssociationMixin, \ - SQLAlchemyNonceMixin, \ - SQLAlchemyCodeMixin, \ - BaseSQLAlchemyStorage - - -SocialBase = declarative_base() - -UID_LENGTH = web.config.get(setting_name('UID_LENGTH'), 255) -User = module_member(web.config[setting_name('USER_MODEL')]) - - -class WebpySocialBase(object): - @classmethod - def _session(cls): - return web.db_session - - -class UserSocialAuth(WebpySocialBase, SQLAlchemyUserMixin, SocialBase): - """Social Auth association model""" - uid = Column(String(UID_LENGTH)) - user_id = Column(Integer, ForeignKey(User.id), - nullable=False, index=True) - user = relationship(User, backref='social_auth') - - @classmethod - def username_max_length(cls): - return User.__table__.columns.get('username').type.length - - @classmethod - def user_model(cls): - return User - - -class Nonce(WebpySocialBase, SQLAlchemyNonceMixin, SocialBase): - """One use numbers""" - pass - - -class Association(WebpySocialBase, SQLAlchemyAssociationMixin, SocialBase): - """OpenId account association""" - pass - - -class Code(WebpySocialBase, SQLAlchemyCodeMixin, SocialBase): - pass - - -class WebpyStorage(BaseSQLAlchemyStorage): - user = UserSocialAuth - nonce = Nonce - association = Association - code = Code +from social_webpy.models import WebpySocialBase, UserSocialAuth, Nonce, Association, Code, WebpyStorage diff --git a/social/apps/webpy_app/utils.py b/social/apps/webpy_app/utils.py index b02035e93..f7846f07c 100644 --- a/social/apps/webpy_app/utils.py +++ b/social/apps/webpy_app/utils.py @@ -1,69 +1 @@ -import warnings - -from functools import wraps - -import web - -from social.utils import setting_name, module_member -from social.backends.utils import get_backend, user_backends_data -from social.strategies.utils import get_strategy - - -DEFAULTS = { - 'STRATEGY': 'social.strategies.webpy_strategy.WebpyStrategy', - 'STORAGE': 'social.apps.webpy_app.models.WebpyStorage' -} - - -def get_helper(name, do_import=False): - config = web.config.get(setting_name(name), - DEFAULTS.get(name, None)) - return do_import and module_member(config) or config - - -def load_strategy(): - return get_strategy(get_helper('STRATEGY'), get_helper('STORAGE')) - - -def load_backend(strategy, name, redirect_uri): - backends = get_helper('AUTHENTICATION_BACKENDS') - Backend = get_backend(backends, name) - return Backend(strategy, redirect_uri) - - -def psa(redirect_uri=None): - def decorator(func): - @wraps(func) - def wrapper(self, backend, *args, **kwargs): - uri = redirect_uri - if uri and backend and '%(backend)s' in uri: - uri = uri % {'backend': backend} - self.strategy = load_strategy() - self.backend = load_backend(self.strategy, backend, uri) - return func(self, backend, *args, **kwargs) - return wrapper - return decorator - - -def backends(user): - """Load Social Auth current user data to context under the key 'backends'. - Will return the output of social.backends.utils.user_backends_data.""" - return user_backends_data(user, get_helper('AUTHENTICATION_BACKENDS'), - get_helper('STORAGE', do_import=True)) - - -def login_redirect(): - """Load current redirect to context.""" - method = web.ctx.method == 'POST' and 'post' or 'get' - data = web.input(_method=method) - value = data.get('next') - return { - 'REDIRECT_FIELD_NAME': 'next', - 'REDIRECT_FIELD_VALUE': value, - 'REDIRECT_QUERYSTRING': value and ('next=' + value) or '' - } - - -def strategy(*args, **kwargs): - warnings.warn('@strategy decorator is deprecated, use @psa instead') - return psa(*args, **kwargs) +from social_webpy.utils import get_helper, load_strategy, load_backend, psa, backends, login_redirect, strategy diff --git a/social/backends/amazon.py b/social/backends/amazon.py index eb1d8ff79..785c633e0 100644 --- a/social/backends/amazon.py +++ b/social/backends/amazon.py @@ -1,45 +1 @@ -""" -Amazon OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/amazon.html -""" -import ssl - -from social.backends.oauth import BaseOAuth2 - - -class AmazonOAuth2(BaseOAuth2): - name = 'amazon' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://www.amazon.com/ap/oa' - ACCESS_TOKEN_URL = 'https://api.amazon.com/auth/o2/token' - DEFAULT_SCOPE = ['profile'] - REDIRECT_STATE = False - ACCESS_TOKEN_METHOD = 'POST' - SSL_PROTOCOL = ssl.PROTOCOL_TLSv1 - EXTRA_DATA = [ - ('refresh_token', 'refresh_token', True), - ('user_id', 'user_id'), - ('postal_code', 'postal_code') - ] - - def get_user_details(self, response): - """Return user details from amazon account""" - name = response.get('name') or '' - fullname, first_name, last_name = self.get_user_names(name) - return {'username': name, - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Grab user profile information from amazon.""" - response = self.get_json('https://www.amazon.com/ap/user/profile', - params={'access_token': access_token}) - if 'Profile' in response: - response = { - 'user_id': response['Profile']['CustomerId'], - 'name': response['Profile']['Name'], - 'email': response['Profile']['PrimaryEmail'] - } - return response +from social_core.backends.amazon import AmazonOAuth2 diff --git a/social/backends/angel.py b/social/backends/angel.py index 4f0a7082c..84c400177 100644 --- a/social/backends/angel.py +++ b/social/backends/angel.py @@ -1,30 +1 @@ -""" -Angel OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/angel.html -""" -from social.backends.oauth import BaseOAuth2 - - -class AngelOAuth2(BaseOAuth2): - name = 'angel' - AUTHORIZATION_URL = 'https://angel.co/api/oauth/authorize/' - ACCESS_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_URL = 'https://angel.co/api/oauth/token/' - REDIRECT_STATE = False - - def get_user_details(self, response): - """Return user details from Angel account""" - username = response['angellist_url'].split('/')[-1] - email = response.get('email', '') - fullname, first_name, last_name = self.get_user_names(response['name']) - return {'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.angel.co/1/me/', params={ - 'access_token': access_token - }) +from social_core.backends.angel import AngelOAuth2 diff --git a/social/backends/aol.py b/social/backends/aol.py index 26c901f30..0b5b8e115 100644 --- a/social/backends/aol.py +++ b/social/backends/aol.py @@ -1,10 +1 @@ -""" -AOL OpenId backend, docs at: - http://psa.matiasaguirre.net/docs/backends/aol.html -""" -from social.backends.open_id import OpenIdAuth - - -class AOLOpenId(OpenIdAuth): - name = 'aol' - URL = 'http://openid.aol.com' +from social_core.backends.aol import AOLOpenId diff --git a/social/backends/appsfuel.py b/social/backends/appsfuel.py index fbe086d64..95935ffe6 100644 --- a/social/backends/appsfuel.py +++ b/social/backends/appsfuel.py @@ -1,42 +1 @@ -""" -Appsfueld OAuth2 backend (with sandbox mode support), docs at: - http://psa.matiasaguirre.net/docs/backends/appsfuel.html -""" -from social.backends.oauth import BaseOAuth2 - - -class AppsfuelOAuth2(BaseOAuth2): - name = 'appsfuel' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'http://app.appsfuel.com/content/permission' - ACCESS_TOKEN_URL = 'https://api.appsfuel.com/v1/live/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - USER_DETAILS_URL = 'https://api.appsfuel.com/v1/live/user' - - def get_user_details(self, response): - """Return user details from Appsfuel account""" - email = response.get('email', '') - username = email.split('@')[0] if email else '' - fullname, first_name, last_name = self.get_user_names( - response.get('display_name', '') - ) - return { - 'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json(self.USER_DETAILS_URL, params={ - 'access_token': access_token - }) - - -class AppsfuelOAuth2Sandbox(AppsfuelOAuth2): - name = 'appsfuel-sandbox' - AUTHORIZATION_URL = 'https://api.appsfuel.com/v1/sandbox/choose' - ACCESS_TOKEN_URL = 'https://api.appsfuel.com/v1/sandbox/oauth/token' - USER_DETAILS_URL = 'https://api.appsfuel.com/v1/sandbox/user' +from social_core.backends.appsfuel import AppsfuelOAuth2, AppsfuelOAuth2Sandbox diff --git a/social/backends/arcgis.py b/social/backends/arcgis.py index 1a2b59ac7..a5f4450bf 100644 --- a/social/backends/arcgis.py +++ b/social/backends/arcgis.py @@ -1,33 +1 @@ -""" -ArcGIS OAuth2 backend -""" -from social.backends.oauth import BaseOAuth2 - - -class ArcGISOAuth2(BaseOAuth2): - name = 'arcgis' - ID_KEY = 'username' - AUTHORIZATION_URL = 'https://www.arcgis.com/sharing/rest/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://www.arcgis.com/sharing/rest/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('expires_in', 'expires_in') - ] - - def get_user_details(self, response): - """Return user details from ArcGIS account""" - return {'username': response.get('username'), - 'email': response.get('email'), - 'fullname': response.get('fullName'), - 'first_name': response.get('firstName'), - 'last_name': response.get('lastName')} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://www.arcgis.com/sharing/rest/community/self', - params={ - 'token': access_token, - 'f': 'json' - } - ) +from social_core.backends.arcgis import ArcGISOAuth2 diff --git a/social/backends/azuread.py b/social/backends/azuread.py index 3c2be305f..19ecf7e3e 100644 --- a/social/backends/azuread.py +++ b/social/backends/azuread.py @@ -1,120 +1 @@ -""" -Copyright (c) 2015 Microsoft Open Technologies, Inc. - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -""" -Azure AD OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/azuread.html -""" -import time - -from jwt import DecodeError, ExpiredSignature, decode as jwt_decode - -from social.exceptions import AuthTokenError -from social.backends.oauth import BaseOAuth2 - - -class AzureADOAuth2(BaseOAuth2): - name = 'azuread-oauth2' - SCOPE_SEPARATOR = ' ' - AUTHORIZATION_URL = 'https://login.microsoftonline.com/common/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - DEFAULT_SCOPE = ['openid', 'profile', 'user_impersonation'] - EXTRA_DATA = [ - ('access_token', 'access_token'), - ('id_token', 'id_token'), - ('refresh_token', 'refresh_token'), - ('expires_in', 'expires'), - ('expires_on', 'expires_on'), - ('not_before', 'not_before'), - ('given_name', 'first_name'), - ('family_name', 'last_name'), - ('token_type', 'token_type') - ] - - def get_user_id(self, details, response): - """Use upn as unique id""" - return response.get('upn') - - def get_user_details(self, response): - """Return user details from Azure AD account""" - fullname, first_name, last_name = ( - response.get('name', ''), - response.get('given_name', ''), - response.get('family_name', '') - ) - return {'username': fullname, - 'email': response.get('upn'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - response = kwargs.get('response') - id_token = response.get('id_token') - try: - decoded_id_token = jwt_decode(id_token, verify=False) - except (DecodeError, ExpiredSignature) as de: - raise AuthTokenError(self, de) - return decoded_id_token - - def auth_extra_arguments(self): - """Return extra arguments needed on auth process. The defaults can be - overriden by GET parameters.""" - extra_arguments = {} - resource = self.setting('RESOURCE') - if resource: - extra_arguments = {'resource': resource} - return extra_arguments - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return access_token and extra defined names to store in - extra_data field""" - data = super(AzureADOAuth2, self).extra_data(user, uid, response, - details, *args, **kwargs) - data['resource'] = self.setting('RESOURCE') - return data - - def refresh_token_params(self, token, *args, **kwargs): - return { - 'client_id': self.setting('KEY'), - 'client_secret': self.setting('SECRET'), - 'refresh_token': token, - 'grant_type': 'refresh_token', - 'resource': self.setting('RESOURCE') - } - - def get_auth_token(self, user_id): - """Return the access token for the given user, after ensuring that it - has not expired, or refreshing it if so.""" - user = self.get_user(user_id=user_id) - access_token = user.social_user.access_token - expires_on = user.social_user.extra_data['expires_on'] - if expires_on <= int(time.time()): - new_token_response = self.refresh_token(token=access_token) - access_token = new_token_response['access_token'] - return access_token +from social_core.backends.azuread import AzureADOAuth2 diff --git a/social/backends/base.py b/social/backends/base.py index 059fe4c9b..e1f0e006f 100644 --- a/social/backends/base.py +++ b/social/backends/base.py @@ -1,238 +1 @@ -from requests import request, ConnectionError - -from social.utils import SSLHttpAdapter, module_member, parse_qs, user_agent -from social.exceptions import AuthFailed - - -class BaseAuth(object): - """A django.contrib.auth backend that authenticates the user based on - a authentication provider response""" - name = '' # provider name, it's stored in database - supports_inactive_user = False # Django auth - ID_KEY = None - EXTRA_DATA = None - REQUIRES_EMAIL_VALIDATION = False - SEND_USER_AGENT = False - SSL_PROTOCOL = None - - def __init__(self, strategy=None, redirect_uri=None): - self.strategy = strategy - self.redirect_uri = redirect_uri - self.data = {} - if strategy: - self.data = self.strategy.request_data() - self.redirect_uri = self.strategy.absolute_uri( - self.redirect_uri - ) - - def setting(self, name, default=None): - """Return setting value from strategy""" - return self.strategy.setting(name, default=default, backend=self) - - def start(self): - # Clean any partial pipeline info before starting the process - self.strategy.clean_partial_pipeline() - if self.uses_redirect(): - return self.strategy.redirect(self.auth_url()) - else: - return self.strategy.html(self.auth_html()) - - def complete(self, *args, **kwargs): - return self.auth_complete(*args, **kwargs) - - def auth_url(self): - """Must return redirect URL to auth provider""" - raise NotImplementedError('Implement in subclass') - - def auth_html(self): - """Must return login HTML content returned by provider""" - raise NotImplementedError('Implement in subclass') - - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - raise NotImplementedError('Implement in subclass') - - def process_error(self, data): - """Process data for errors, raise exception if needed. - Call this method on any override of auth_complete.""" - pass - - def authenticate(self, *args, **kwargs): - """Authenticate user using social credentials - - Authentication is made if this is the correct backend, backend - verification is made by kwargs inspection for current backend - name presence. - """ - # Validate backend and arguments. Require that the Social Auth - # response be passed in as a keyword argument, to make sure we - # don't match the username/password calling conventions of - # authenticate. - if 'backend' not in kwargs or kwargs['backend'].name != self.name or \ - 'strategy' not in kwargs or 'response' not in kwargs: - return None - - self.strategy = self.strategy or kwargs.get('strategy') - self.redirect_uri = self.redirect_uri or kwargs.get('redirect_uri') - self.data = self.strategy.request_data() - pipeline = self.strategy.get_pipeline() - kwargs.setdefault('is_new', False) - if 'pipeline_index' in kwargs: - pipeline = pipeline[kwargs['pipeline_index']:] - return self.pipeline(pipeline, *args, **kwargs) - - def pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): - out = self.run_pipeline(pipeline, pipeline_index, *args, **kwargs) - if not isinstance(out, dict): - return out - user = out.get('user') - if user: - user.social_user = out.get('social') - user.is_new = out.get('is_new') - return user - - def disconnect(self, *args, **kwargs): - pipeline = self.strategy.get_disconnect_pipeline() - if 'pipeline_index' in kwargs: - pipeline = pipeline[kwargs['pipeline_index']:] - kwargs['name'] = self.name - kwargs['user_storage'] = self.strategy.storage.user - return self.run_pipeline(pipeline, *args, **kwargs) - - def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): - out = kwargs.copy() - out.setdefault('strategy', self.strategy) - out.setdefault('backend', out.pop(self.name, None) or self) - out.setdefault('request', self.strategy.request_data()) - out.setdefault('details', {}) - - for idx, name in enumerate(pipeline): - out['pipeline_index'] = pipeline_index + idx - func = module_member(name) - result = func(*args, **out) or {} - if not isinstance(result, dict): - return result - out.update(result) - self.strategy.clean_partial_pipeline() - return out - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return default extra data to store in extra_data field""" - data = {} - for entry in (self.EXTRA_DATA or []) + self.setting('EXTRA_DATA', []): - if not isinstance(entry, (list, tuple)): - entry = (entry,) - size = len(entry) - if size >= 1 and size <= 3: - if size == 3: - name, alias, discard = entry - elif size == 2: - (name, alias), discard = entry, False - elif size == 1: - name = alias = entry[0] - discard = False - value = response.get(name) or details.get(name) - if discard and not value: - continue - data[alias] = value - return data - - def auth_allowed(self, response, details): - """Return True if the user should be allowed to authenticate, by - default check if email is whitelisted (if there's a whitelist)""" - emails = self.setting('WHITELISTED_EMAILS', []) - domains = self.setting('WHITELISTED_DOMAINS', []) - email = details.get('email') - allowed = True - if email and (emails or domains): - domain = email.split('@', 1)[1] - allowed = email in emails or domain in domains - return allowed - - def get_user_id(self, details, response): - """Return a unique ID for the current user, by default from server - response.""" - return response.get(self.ID_KEY) - - def get_user_details(self, response): - """Must return user details in a know internal struct: - {'username': , - 'email': , - 'fullname': , - 'first_name': , - 'last_name': } - """ - raise NotImplementedError('Implement in subclass') - - def get_user_names(self, fullname='', first_name='', last_name=''): - # Avoid None values - fullname = fullname or '' - first_name = first_name or '' - last_name = last_name or '' - if fullname and not (first_name or last_name): - try: - first_name, last_name = fullname.split(' ', 1) - except ValueError: - first_name = first_name or fullname or '' - last_name = last_name or '' - fullname = fullname or ' '.join((first_name, last_name)) - return fullname.strip(), first_name.strip(), last_name.strip() - - def get_user(self, user_id): - """ - Return user with given ID from the User model used by this backend. - This is called by django.contrib.auth.middleware. - """ - from social.strategies.utils import get_current_strategy - strategy = self.strategy or get_current_strategy() - return strategy.get_user(user_id) - - def continue_pipeline(self, *args, **kwargs): - """Continue previous halted pipeline""" - kwargs.update({'backend': self, 'strategy': self.strategy}) - return self.authenticate(*args, **kwargs) - - def auth_extra_arguments(self): - """Return extra arguments needed on auth process. The defaults can be - overridden by GET parameters.""" - extra_arguments = self.setting('AUTH_EXTRA_ARGUMENTS', {}).copy() - extra_arguments.update((key, self.data[key]) for key in extra_arguments - if key in self.data) - return extra_arguments - - def uses_redirect(self): - """Return True if this provider uses redirect url method, - otherwise return false.""" - return True - - def request(self, url, method='GET', *args, **kwargs): - kwargs.setdefault('headers', {}) - if self.setting('VERIFY_SSL') is not None: - kwargs.setdefault('verify', self.setting('VERIFY_SSL')) - kwargs.setdefault('timeout', self.setting('REQUESTS_TIMEOUT') or - self.setting('URLOPEN_TIMEOUT')) - if self.SEND_USER_AGENT and 'User-Agent' not in kwargs['headers']: - kwargs['headers']['User-Agent'] = user_agent() - - try: - if self.SSL_PROTOCOL: - session = SSLHttpAdapter.ssl_adapter_session(self.SSL_PROTOCOL) - response = session.request(method, url, *args, **kwargs) - else: - response = request(method, url, *args, **kwargs) - except ConnectionError as err: - raise AuthFailed(self, str(err)) - response.raise_for_status() - return response - - def get_json(self, url, *args, **kwargs): - return self.request(url, *args, **kwargs).json() - - def get_querystring(self, url, *args, **kwargs): - return parse_qs(self.request(url, *args, **kwargs).text) - - def get_key_and_secret(self): - """Return tuple with Consumer Key and Consumer Secret for current - service provider. Must return (key, secret), order *must* be respected. - """ - return self.setting('KEY'), self.setting('SECRET') +from social_core.backends.base import BaseAuth diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py index eefd2fa5f..78e3f713f 100644 --- a/social/backends/battlenet.py +++ b/social/backends/battlenet.py @@ -1,50 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -# This provides a backend for python-social-auth. This should not be confused -# with officially battle.net offerings. This piece of code is not officially -# affiliated with Blizzard Entertainment, copyrights to their respective -# owners. See http://us.battle.net/en/forum/topic/13979588015 for more details. - - -class BattleNetOAuth2(BaseOAuth2): - """ battle.net Oauth2 backend""" - name = 'battlenet-oauth2' - ID_KEY = 'accountId' - REDIRECT_STATE = False - AUTHORIZATION_URL = 'https://eu.battle.net/oauth/authorize' - ACCESS_TOKEN_URL = 'https://eu.battle.net/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['wow.profile'] - EXTRA_DATA = [ - ('refresh_token', 'refresh_token', True), - ('expires_in', 'expires'), - ('token_type', 'token_type', True) - ] - - def get_characters(self, access_token): - """ - Fetches the character list from the battle.net API. Returns list of - characters or empty list if the request fails. - """ - params = {'access_token': access_token} - if self.setting('API_LOCALE'): - params['locale'] = self.setting('API_LOCALE') - - response = self.get_json( - 'https://eu.api.battle.net/wow/user/characters', - params=params - ) - return response.get('characters') or [] - - def get_user_details(self, response): - """ Return user details from Battle.net account """ - return {'battletag': response.get('battletag')} - - def user_data(self, access_token, *args, **kwargs): - """ Loads user data from service """ - return self.get_json( - 'https://eu.api.battle.net/account/user', - params={'access_token': access_token} - ) +from social_core.backends.battlenet import BattleNetOAuth2 diff --git a/social/backends/beats.py b/social/backends/beats.py index d1d877252..7402998dc 100644 --- a/social/backends/beats.py +++ b/social/backends/beats.py @@ -1,65 +1 @@ -""" -Beats backend, docs at: - https://developer.beatsmusic.com/docs -""" -import base64 - -from social.utils import handle_http_errors -from social.backends.oauth import BaseOAuth2 - - -class BeatsOAuth2(BaseOAuth2): - name = 'beats' - SCOPE_SEPARATOR = ' ' - ID_KEY = 'user_context' - AUTHORIZATION_URL = \ - 'https://partner.api.beatsmusic.com/v1/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://partner.api.beatsmusic.com/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def get_user_id(self, details, response): - return response['result'][BeatsOAuth2.ID_KEY] - - def auth_headers(self): - return { - 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( - ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) - )) - } - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - self.process_error(self.data) - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - # mashery wraps in jsonrpc - if response.get('jsonrpc', None): - response = response.get('result', None) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) - - def get_user_details(self, response): - """Return user details from Beats account""" - response = response['result'] - fullname, first_name, last_name = self.get_user_names( - response.get('display_name') - ) - return {'username': response.get('id'), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://partner.api.beatsmusic.com/v1/api/me', - headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) +from social_core.backends.beats import BeatsOAuth2 diff --git a/social/backends/behance.py b/social/backends/behance.py index 8d98d41fe..a790df186 100644 --- a/social/backends/behance.py +++ b/social/backends/behance.py @@ -1,40 +1 @@ -""" -Behance OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/behance.html -""" -from social.backends.oauth import BaseOAuth2 - - -class BehanceOAuth2(BaseOAuth2): - """Behance OAuth authentication backend""" - name = 'behance' - AUTHORIZATION_URL = 'https://www.behance.net/v2/oauth/authenticate' - ACCESS_TOKEN_URL = 'https://www.behance.net/v2/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = '|' - EXTRA_DATA = [('username', 'username')] - REDIRECT_STATE = False - - def get_user_id(self, details, response): - return response['user']['id'] - - def get_user_details(self, response): - """Return user details from Behance account""" - user = response['user'] - fullname, first_name, last_name = self.get_user_names( - user['display_name'], user['first_name'], user['last_name'] - ) - return {'username': user['username'], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': ''} - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - # Pull up the embedded user attributes so they can be found as extra - # data. See the example token response for possible attributes: - # http://www.behance.net/dev/authentication#step-by-step - data = response.copy() - data.update(response['user']) - return super(BehanceOAuth2, self).extra_data(user, uid, data, details, - *args, **kwargs) +from social_core.backends.behance import BehanceOAuth2 diff --git a/social/backends/belgiumeid.py b/social/backends/belgiumeid.py index 1ff44270d..296d8bc3f 100644 --- a/social/backends/belgiumeid.py +++ b/social/backends/belgiumeid.py @@ -1,11 +1 @@ -""" -Belgium EID OpenId backend, docs at: - http://psa.matiasaguirre.net/docs/backends/belgium_eid.html -""" -from social.backends.open_id import OpenIdAuth - - -class BelgiumEIDOpenId(OpenIdAuth): - """Belgium e-ID OpenID authentication backend""" - name = 'belgiumeid' - URL = 'https://www.e-contract.be/eid-idp/endpoints/openid/auth' +from social_core.backends.belgiumeid import BelgiumEIDOpenId diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index fd6d02c93..fb3862e40 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -1,102 +1,2 @@ -""" -Bitbucket OAuth2 and OAuth1 backends, docs at: - http://psa.matiasaguirre.net/docs/backends/bitbucket.html -""" -from social.exceptions import AuthForbidden -from social.backends.oauth import BaseOAuth1, BaseOAuth2 - - -class BitbucketOAuthBase(object): - ID_KEY = 'uuid' - - def get_user_id(self, details, response): - id_key = self.ID_KEY - if self.setting('USERNAME_AS_ID', False): - id_key = 'username' - return response.get(id_key) - - def get_user_details(self, response): - """Return user details from Bitbucket account""" - fullname, first_name, last_name = self.get_user_names( - response['display_name'] - ) - - return {'username': response.get('username', ''), - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - emails = self._get_emails(access_token) - email = None - - for address in reversed(emails['values']): - email = address['email'] - if address['is_primary']: - break - - if self.setting('VERIFIED_EMAILS_ONLY', False) and \ - not address['is_confirmed']: - raise AuthForbidden(self, 'Bitbucket account has no verified email') - - user = self._get_user(access_token) - if email: - user['email'] = email - return user - - def _get_user(self, access_token=None): - raise NotImplementedError('Implement in subclass') - - def _get_emails(self, access_token=None): - raise NotImplementedError('Implement in subclass') - - -class BitbucketOAuth2(BitbucketOAuthBase, BaseOAuth2): - name = 'bitbucket-oauth2' - SCOPE_SEPARATOR = ' ' - AUTHORIZATION_URL = 'https://bitbucket.org/site/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('scopes', 'scopes'), - ('expires_in', 'expires'), - ('token_type', 'token_type'), - ('refresh_token', 'refresh_token') - ] - - def auth_complete_credentials(self): - return self.get_key_and_secret() - - def _get_user(self, access_token=None): - return self.get_json('https://api.bitbucket.org/2.0/user', - params={'access_token': access_token}) - - def _get_emails(self, access_token=None): - return self.get_json('https://api.bitbucket.org/2.0/user/emails', - params={'access_token': access_token}) - - def refresh_token(self, *args, **kwargs): - raise NotImplementedError('Refresh tokens for Bitbucket have ' - 'not been implemented') - - -class BitbucketOAuth(BitbucketOAuthBase, BaseOAuth1): - """Bitbucket OAuth authentication backend""" - name = 'bitbucket' - AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' - REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' - ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' - - def oauth_auth(self, *args, **kwargs): - return super(BitbucketOAuth, self).oauth_auth(*args, **kwargs) - - def _get_user(self, access_token=None): - return self.get_json('https://api.bitbucket.org/2.0/user', - auth=self.oauth_auth(access_token)) - - def _get_emails(self, access_token=None): - return self.get_json('https://api.bitbucket.org/2.0/user/emails', - auth=self.oauth_auth(access_token)) +from social_core.backends.bitbucket import BitbucketOAuthBase, \ + BitbucketOAuth2, BitbucketOAuth diff --git a/social/backends/box.py b/social/backends/box.py index a13eaa26b..f4bc15b80 100644 --- a/social/backends/box.py +++ b/social/backends/box.py @@ -1,55 +1 @@ -""" -Box.net OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/box.html -""" -from social.backends.oauth import BaseOAuth2 - - -class BoxOAuth2(BaseOAuth2): - """Box.net OAuth authentication backend""" - name = 'box' - AUTHORIZATION_URL = 'https://www.box.com/api/oauth2/authorize' - ACCESS_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_URL = 'https://www.box.com/api/oauth2/token' - REVOKE_TOKEN_URL = 'https://www.box.com/api/oauth2/revoke' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('refresh_token', 'refresh_token', True), - ('id', 'id'), - ('expires', 'expires'), - ] - - def do_auth(self, access_token, response=None, *args, **kwargs): - response = response or {} - data = self.user_data(access_token) - - data['access_token'] = response.get('access_token') - data['refresh_token'] = response.get('refresh_token') - data['expires'] = response.get('expires_in') - kwargs.update({'backend': self, 'response': data}) - return self.strategy.authenticate(*args, **kwargs) - - def get_user_details(self, response): - """Return user details Box.net account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - return {'username': response.get('login'), - 'email': response.get('login') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - params = self.setting('PROFILE_EXTRA_PARAMS', {}) - params['access_token'] = access_token - return self.get_json('https://api.box.com/2.0/users/me', - params=params) - - def refresh_token(self, token, *args, **kwargs): - params = self.refresh_token_params(token, *args, **kwargs) - request = self.request(self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL, - data=params, headers=self.auth_headers(), - method='POST') - return self.process_refresh_token_response(request, *args, **kwargs) +from social_core.backends.box import BoxOAuth2 diff --git a/social/backends/changetip.py b/social/backends/changetip.py index dfb8206bf..9340f396f 100644 --- a/social/backends/changetip.py +++ b/social/backends/changetip.py @@ -1,27 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class ChangeTipOAuth2(BaseOAuth2): - """ChangeTip OAuth authentication backend - https://www.changetip.com/api - """ - name = 'changetip' - AUTHORIZATION_URL = 'https://www.changetip.com/o/authorize/' - ACCESS_TOKEN_URL = 'https://www.changetip.com/o/token/' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ' ' - - def get_user_details(self, response): - """Return user details from ChangeTip account""" - return { - 'username': response['username'], - 'email': response.get('email', ''), - 'first_name': '', - 'last_name': '', - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.changetip.com/v2/me/', params={ - 'access_token': access_token - }) +from social_core.backends.changetip import ChangeTipOAuth2 diff --git a/social/backends/classlink.py b/social/backends/classlink.py index 31f8f74d2..da7209405 100644 --- a/social/backends/classlink.py +++ b/social/backends/classlink.py @@ -1,44 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class ClasslinkOAuth(BaseOAuth2): - """ - Classlink OAuth authentication backend. - - Docs: https://developer.classlink.com/docs/oauth2-workflow - """ - name = 'classlink' - AUTHORIZATION_URL = 'https://launchpad.classlink.com/oauth2/v2/auth' - ACCESS_TOKEN_URL = 'https://launchpad.classlink.com/oauth2/v2/token' - ACCESS_TOKEN_METHOD = 'POST' - DEFAULT_SCOPE = ['profile'] - REDIRECT_STATE = False - SCOPE_SEPARATOR = ' ' - - def get_user_id(self, details, response): - """Return user unique id provided by service""" - return response['UserId'] - - def get_user_details(self, response): - """Return user details from Classlink account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('FirstName'), - last_name=response.get('LastName') - ) - - return { - 'username': response.get('Email') or response.get('LoginId'), - 'email': response.get('Email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - } - - def user_data(self, token, *args, **kwargs): - """Loads user data from service""" - url = 'https://nodeapi.classlink.com/v2/my/info' - auth_header = {"Authorization": "Bearer %s" % token} - try: - return self.get_json(url, headers=auth_header) - except ValueError: - return None +from social_core.backends.classlink import ClasslinkOAuth diff --git a/social/backends/clef.py b/social/backends/clef.py index c1beff533..33d948b27 100644 --- a/social/backends/clef.py +++ b/social/backends/clef.py @@ -1,54 +1 @@ -""" -Clef OAuth support. - -This contribution adds support for Clef OAuth service. The settings -SOCIAL_AUTH_CLEF_KEY and SOCIAL_AUTH_CLEF_SECRET must be defined with the -values given by Clef application registration process. -""" - -from social.backends.oauth import BaseOAuth2 - - -class ClefOAuth2(BaseOAuth2): - """Clef OAuth authentication backend""" - name = 'clef' - AUTHORIZATION_URL = 'https://clef.io/iframes/qr' - ACCESS_TOKEN_URL = 'https://clef.io/api/v1/authorize' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - - def auth_params(self, *args, **kwargs): - params = super(ClefOAuth2, self).auth_params(*args, **kwargs) - params['app_id'] = params.pop('client_id') - params['redirect_url'] = params.pop('redirect_uri') - return params - - def get_user_id(self, response, details): - return details.get('info').get('id') - - def get_user_details(self, response): - """Return user details from Github account""" - info = response.get('info') - fullname, first_name, last_name = self.get_user_names( - first_name=info.get('first_name'), - last_name=info.get('last_name') - ) - - email = info.get('email', '') - if email: - username = email.split('@', 1)[0] - else: - username = info.get('id') - - return { - 'username': username, - 'email': email, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'phone_number': info.get('phone_number', '') - } - - def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://clef.io/api/v1/info', - params={'access_token': access_token}) +from social_core.backends.clef import ClefOAuth2 diff --git a/social/backends/coding.py b/social/backends/coding.py index 3b4295eae..83c0bf597 100644 --- a/social/backends/coding.py +++ b/social/backends/coding.py @@ -1,48 +1 @@ -""" -Coding OAuth2 backend, docs at: -""" -from six.moves.urllib.parse import urljoin - -from social.backends.oauth import BaseOAuth2 - - -class CodingOAuth2(BaseOAuth2): - """Coding OAuth authentication backend""" - - name = 'coding' - API_URL = 'https://coding.net/api/' - AUTHORIZATION_URL = 'https://coding.net/oauth_authorize.html' - ACCESS_TOKEN_URL = 'https://coding.net/api/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - DEFAULT_SCOPE = ['user'] - REDIRECT_STATE = False - - def api_url(self): - return self.API_URL - - def get_user_details(self, response): - """Return user details from Github account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - return {'username': response.get('name'), - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - data = self._user_data(access_token) - if data.get('code') != 0: - # 获取失败 - pass - return data.get('data') - - def _user_data(self, access_token, path=None): - url = urljoin( - self.api_url(), - 'account/current_user{0}'.format(path or '') - ) - return self.get_json(url, params={'access_token': access_token}) +from social_core.backends.coding import CodingOAuth2 diff --git a/social/backends/coinbase.py b/social/backends/coinbase.py index ae9106f92..eeb3433f8 100644 --- a/social/backends/coinbase.py +++ b/social/backends/coinbase.py @@ -1,35 +1 @@ -""" -Coinbase OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/coinbase.html -""" -from social.backends.oauth import BaseOAuth2 - - -class CoinbaseOAuth2(BaseOAuth2): - name = 'coinbase' - SCOPE_SEPARATOR = '+' - DEFAULT_SCOPE = ['user', 'balance'] - AUTHORIZATION_URL = 'https://coinbase.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://coinbase.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def get_user_id(self, details, response): - return response['users'][0]['user']['id'] - - def get_user_details(self, response): - """Return user details from Coinbase account""" - user_data = response['users'][0]['user'] - email = user_data.get('email', '') - name = user_data['name'] - fullname, first_name, last_name = self.get_user_names(name) - return {'username': name, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://coinbase.com/api/v1/users', - params={'access_token': access_token}) +from social_core.backends.coinbase import CoinbaseOAuth2 diff --git a/social/backends/coursera.py b/social/backends/coursera.py index ed32721ee..ad61e7969 100644 --- a/social/backends/coursera.py +++ b/social/backends/coursera.py @@ -1,43 +1 @@ -""" -Coursera OAuth2 backend, docs at: - https://tech.coursera.org/app-platform/oauth2/ -""" -from social.backends.oauth import BaseOAuth2 - - -class CourseraOAuth2(BaseOAuth2): - """Coursera OAuth2 authentication backend""" - name = 'coursera' - ID_KEY = 'username' - AUTHORIZATION_URL = 'https://accounts.coursera.org/oauth2/v1/auth' - ACCESS_TOKEN_URL = 'https://accounts.coursera.org/oauth2/v1/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - SCOPE_SEPARATOR = ',' - DEFAULT_SCOPE = ['view_profile'] - - def _get_username_from_response(self, response): - elements = response.get('elements', []) - for element in elements: - if 'id' in element: - return element.get('id') - - return None - - def get_user_details(self, response): - """Return user details from Coursera account""" - return {'username': self._get_username_from_response(response)} - - def get_user_id(self, details, response): - """Return a username prepared in get_user_details as uid""" - return details.get(self.ID_KEY) - - def user_data(self, access_token, *args, **kwargs): - """Load user data from the service""" - return self.get_json( - 'https://api.coursera.org/api/externalBasicProfiles.v1?q=me', - headers=self.get_auth_header(access_token) - ) - - def get_auth_header(self, access_token): - return {'Authorization': 'Bearer {0}'.format(access_token)} +from social_core.backends.coursera import CourseraOAuth2 diff --git a/social/backends/dailymotion.py b/social/backends/dailymotion.py index bef394dc1..c1b55f132 100644 --- a/social/backends/dailymotion.py +++ b/social/backends/dailymotion.py @@ -1,24 +1 @@ -""" -DailyMotion OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/dailymotion.html -""" -from social.backends.oauth import BaseOAuth2 - - -class DailymotionOAuth2(BaseOAuth2): - """Dailymotion OAuth authentication backend""" - name = 'dailymotion' - EXTRA_DATA = [('id', 'id')] - ID_KEY = 'username' - AUTHORIZATION_URL = 'https://api.dailymotion.com/oauth/authorize' - REQUEST_TOKEN_URL = 'https://api.dailymotion.com/oauth/token' - ACCESS_TOKEN_URL = 'https://api.dailymotion.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - - def get_user_details(self, response): - return {'username': response.get('screenname')} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json('https://api.dailymotion.com/me/', - params={'access_token': access_token}) +from social_core.backends.dailymotion import DailymotionOAuth2 diff --git a/social/backends/deezer.py b/social/backends/deezer.py index 947afc065..7ff23a0ba 100644 --- a/social/backends/deezer.py +++ b/social/backends/deezer.py @@ -1,49 +1 @@ -""" -Deezer backend, docs at: - https://developers.deezer.com/api/oauth - https://developers.deezer.com/api/permissions -""" -from six.moves.urllib.parse import parse_qsl - -from social.backends.oauth import BaseOAuth2 - - -class DeezerOAuth2(BaseOAuth2): - """Deezer OAuth2 authentication backend""" - name = 'deezer' - ID_KEY = 'name' - AUTHORIZATION_URL = 'https://connect.deezer.com/oauth/auth.php' - ACCESS_TOKEN_URL = 'https://connect.deezer.com/oauth/access_token.php' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - REDIRECT_STATE = False - - def auth_complete_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - return { - 'app_id': client_id, - 'secret': client_secret, - 'code': self.data.get('code') - } - - def request_access_token(self, *args, **kwargs): - response = self.request(*args, **kwargs) - return dict(parse_qsl(response.text)) - - def get_user_details(self, response): - """Return user details from Deezer account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('firstname'), - last_name=response.get('lastname') - ) - return {'username': response.get('name'), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('http://api.deezer.com/user/me', params={ - 'access_token': access_token - }) +from social_core.backends.deezer import DeezerOAuth2 diff --git a/social/backends/digitalocean.py b/social/backends/digitalocean.py index 780e4be6e..d33063a69 100644 --- a/social/backends/digitalocean.py +++ b/social/backends/digitalocean.py @@ -1,41 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class DigitalOceanOAuth(BaseOAuth2): - """ - DigitalOcean OAuth authentication backend. - - Docs: https://developers.digitalocean.com/documentation/oauth/ - """ - name = 'digitalocean' - AUTHORIZATION_URL = 'https://cloud.digitalocean.com/v1/oauth/authorize' - ACCESS_TOKEN_URL = 'https://cloud.digitalocean.com/v1/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ' ' - EXTRA_DATA = [ - ('expires_in', 'expires_in') - ] - - def get_user_id(self, details, response): - """Return user unique id provided by service""" - return response['account'].get('uuid') - - def get_user_details(self, response): - """Return user details from DigitalOcean account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') or '') - - return {'username': response['account'].get('email'), - 'email': response['account'].get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, token, *args, **kwargs): - """Loads user data from service""" - url = 'https://api.digitalocean.com/v2/account' - auth_header = {"Authorization": "Bearer %s" % token} - try: - return self.get_json(url, headers=auth_header) - except ValueError: - return None +from social_core.backends.digitalocean import DigitalOceanOAuth diff --git a/social/backends/disqus.py b/social/backends/disqus.py index 3542a6bb1..9d5e88bec 100644 --- a/social/backends/disqus.py +++ b/social/backends/disqus.py @@ -1,51 +1 @@ -""" -Disqus OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/disqus.html -""" -from social.backends.oauth import BaseOAuth2 - - -class DisqusOAuth2(BaseOAuth2): - name = 'disqus' - AUTHORIZATION_URL = 'https://disqus.com/api/oauth/2.0/authorize/' - ACCESS_TOKEN_URL = 'https://disqus.com/api/oauth/2.0/access_token/' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('avatar', 'avatar'), - ('connections', 'connections'), - ('user_id', 'user_id'), - ('email', 'email'), - ('email_hash', 'emailHash'), - ('expires', 'expires'), - ('location', 'location'), - ('meta', 'response'), - ('name', 'name'), - ('username', 'username'), - ] - - def get_user_id(self, details, response): - return response['response']['id'] - - def get_user_details(self, response): - """Return user details from Disqus account""" - rr = response.get('response', {}) - return { - 'username': rr.get('username', ''), - 'user_id': response.get('user_id', ''), - 'email': rr.get('email', ''), - 'name': rr.get('name', ''), - } - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - meta_response = dict(response, **response.get('response', {})) - return super(DisqusOAuth2, self).extra_data(user, uid, meta_response, - details, *args, **kwargs) - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - key, secret = self.get_key_and_secret() - return self.get_json( - 'https://disqus.com/api/3.0/users/details.json', - params={'access_token': access_token, 'api_secret': secret} - ) +from social_core.backends.disqus import DisqusOAuth2 diff --git a/social/backends/docker.py b/social/backends/docker.py index 92513ca24..d11f3a601 100644 --- a/social/backends/docker.py +++ b/social/backends/docker.py @@ -1,46 +1 @@ -""" -Docker Hub OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/docker.html -""" -from social.backends.oauth import BaseOAuth2 - - -class DockerOAuth2(BaseOAuth2): - name = 'docker' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://hub.docker.com/api/v1.1/o/authorize/' - ACCESS_TOKEN_URL = 'https://hub.docker.com/api/v1.1/o/token/' - REFRESH_TOKEN_URL = 'https://hub.docker.com/api/v1.1/o/token/' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('refresh_token', 'refresh_token', True), - ('user_id', 'user_id'), - ('email', 'email'), - ('full_name', 'fullname'), - ('location', 'location'), - ('url', 'url'), - ('company', 'company'), - ('gravatar_email', 'gravatar_email'), - ] - - def get_user_details(self, response): - """Return user details from Docker Hub account""" - fullname, first_name, last_name = self.get_user_names( - response.get('full_name') or response.get('username') or '' - ) - return { - 'username': response.get('username'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': response.get('email', '') - } - - def user_data(self, access_token, *args, **kwargs): - """Grab user profile information from Docker Hub.""" - username = kwargs['response']['username'] - return self.get_json( - 'https://hub.docker.com/api/v1.1/users/%s/' % username, - headers={'Authorization': 'Bearer %s' % access_token} - ) +from social_core.backends.docker import DockerOAuth2 diff --git a/social/backends/douban.py b/social/backends/douban.py index f5f1ae613..e9b53623e 100644 --- a/social/backends/douban.py +++ b/social/backends/douban.py @@ -1,59 +1 @@ -""" -Douban OAuth1 and OAuth2 backends, docs at: - http://psa.matiasaguirre.net/docs/backends/douban.html -""" -from social.backends.oauth import BaseOAuth2, BaseOAuth1 - - -class DoubanOAuth(BaseOAuth1): - """Douban OAuth authentication backend""" - name = 'douban' - EXTRA_DATA = [('id', 'id')] - AUTHORIZATION_URL = 'http://www.douban.com/service/auth/authorize' - REQUEST_TOKEN_URL = 'http://www.douban.com/service/auth/request_token' - ACCESS_TOKEN_URL = 'http://www.douban.com/service/auth/access_token' - - def get_user_id(self, details, response): - return response['db:uid']['$t'] - - def get_user_details(self, response): - """Return user details from Douban""" - return {'username': response["db:uid"]["$t"], - 'email': ''} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json('http://api.douban.com/people/%40me?&alt=json', - auth=self.oauth_auth(access_token)) - - -class DoubanOAuth2(BaseOAuth2): - """Douban OAuth authentication backend""" - name = 'douban-oauth2' - AUTHORIZATION_URL = 'https://www.douban.com/service/auth2/auth' - ACCESS_TOKEN_URL = 'https://www.douban.com/service/auth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('id', 'id'), - ('uid', 'username'), - ('refresh_token', 'refresh_token'), - ] - - def get_user_details(self, response): - """Return user details from Douban""" - fullname, first_name, last_name = self.get_user_names( - response.get('name', '') - ) - return {'username': response.get('uid', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': ''} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json( - 'https://api.douban.com/v2/user/~me', - headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) +from social_core.backends.douban import DoubanOAuth, DoubanOAuth2 diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py index ab6d0e5ae..1280009b3 100644 --- a/social/backends/dribbble.py +++ b/social/backends/dribbble.py @@ -1,62 +1 @@ -""" -Dribbble OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/dribbble.html - http://developer.dribbble.com/v1/oauth/ -""" - -from social.backends.oauth import BaseOAuth2 - - -class DribbbleOAuth2(BaseOAuth2): - """Dribbble OAuth authentication backend""" - name = 'dribbble' - AUTHORIZATION_URL = 'https://dribbble.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://dribbble.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('id', 'id'), - ('name', 'name'), - ('html_url', 'html_url'), - ('avatar_url', 'avatar_url'), - ('bio', 'bio'), - ('location', 'location'), - ('links', 'links'), - ('buckets_count', 'buckets_count'), - ('comments_received_count', 'comments_received_count'), - ('followers_count', 'followers_count'), - ('followings_count', 'followings_count'), - ('likes_count', 'likes_count'), - ('likes_received_count', 'likes_received_count'), - ('projects_count', 'projects_count'), - ('rebounds_received_count', 'rebounds_received_count'), - ('shots_count', 'shots_count'), - ('teams_count', 'teams_count'), - ('pro', 'pro'), - ('buckets_url', 'buckets_url'), - ('followers_url', 'followers_url'), - ('following_url', 'following_url'), - ('likes_url', 'shots_url'), - ('teams_url', 'teams_url'), - ('created_at', 'created_at'), - ('updated_at', 'updated_at'), - ] - - def get_user_details(self, response): - """Return user details from Dribbble account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - return {'username': response.get('username'), - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.dribbble.com/v1/user', - headers={ - 'Authorization': 'Bearer {0}'.format(access_token) - }) +from social_core.backends.dribbble import DribbbleOAuth2 diff --git a/social/backends/drip.py b/social/backends/drip.py index 4bfeb95a1..b39261762 100644 --- a/social/backends/drip.py +++ b/social/backends/drip.py @@ -1,25 +1 @@ -""" -Drip OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/drip.html -""" -from social.backends.oauth import BaseOAuth2 - - -class DripOAuth(BaseOAuth2): - name = 'drip' - AUTHORIZATION_URL = 'https://www.getdrip.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://www.getdrip.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - - def get_user_id(self, details, response): - return details['email'] - - def get_user_details(self, response): - return {'email': response['users'][0]['email'], - 'fullname': response['users'][0]['name'], - 'username': response['users'][0]['email']} - - def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://api.getdrip.com/v2/user', headers={ - 'Authorization': 'Bearer %s' % access_token - }) +from social_core.backends.drip import DripOAuth diff --git a/social/backends/dropbox.py b/social/backends/dropbox.py index a4e4baf5f..4aae11faa 100644 --- a/social/backends/dropbox.py +++ b/social/backends/dropbox.py @@ -1,67 +1 @@ -""" -Dropbox OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/dropbox.html -""" -from social.backends.oauth import BaseOAuth1, BaseOAuth2 - - -class DropboxOAuth(BaseOAuth1): - """Dropbox OAuth authentication backend""" - name = 'dropbox' - ID_KEY = 'uid' - AUTHORIZATION_URL = 'https://www.dropbox.com/1/oauth/authorize' - REQUEST_TOKEN_URL = 'https://api.dropbox.com/1/oauth/request_token' - REQUEST_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_URL = 'https://api.dropbox.com/1/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Dropbox account""" - fullname, first_name, last_name = self.get_user_names( - response.get('display_name') - ) - return {'username': str(response.get('uid')), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.dropbox.com/1/account/info', - auth=self.oauth_auth(access_token)) - - -class DropboxOAuth2(BaseOAuth2): - name = 'dropbox-oauth2' - ID_KEY = 'uid' - AUTHORIZATION_URL = 'https://www.dropbox.com/1/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://api.dropbox.com/1/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('uid', 'username'), - ] - - def get_user_details(self, response): - """Return user details from Dropbox account""" - fullname, first_name, last_name = self.get_user_names( - response.get('display_name') - ) - return {'username': str(response.get('uid')), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.dropbox.com/1/account/info', - headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) +from social_core.backends.dropbox import DropboxOAuth, DropboxOAuth2 diff --git a/social/backends/echosign.py b/social/backends/echosign.py index 15c6887c3..d51ee4ab0 100644 --- a/social/backends/echosign.py +++ b/social/backends/echosign.py @@ -1,24 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class EchosignOAuth2(BaseOAuth2): - name = 'echosign' - REDIRECT_STATE = False - ACCESS_TOKEN_METHOD = 'POST' - REFRESH_TOKEN_METHOD = 'POST' - REVOKE_TOKEN_METHOD = 'POST' - AUTHORIZATION_URL = 'https://secure.echosign.com/public/oauth' - ACCESS_TOKEN_URL = 'https://secure.echosign.com/oauth/token' - REFRESH_TOKEN_URL = 'https://secure.echosign.com/oauth/refresh' - REVOKE_TOKEN_URL = 'https://secure.echosign.com/oauth/revoke' - - def get_user_details(self, response): - return response - - def get_user_id(self, details, response): - return details['userInfoList'][0]['userId'] - - def user_data(self, access_token, *args, **kwargs): - return self.get_json( - 'https://api.echosign.com/api/rest/v3/users', - headers={'Access-Token': access_token}) +from social_core.backends.echosign import EchosignOAuth2 diff --git a/social/backends/edmodo.py b/social/backends/edmodo.py index cc735897d..d052a6dfb 100644 --- a/social/backends/edmodo.py +++ b/social/backends/edmodo.py @@ -1,34 +1 @@ -""" -Edmodo OAuth2 Sign-in backend, docs at: - http://psa.matiasaguirre.net/docs/backends/edmodo.html -""" -from social.backends.oauth import BaseOAuth2 - - -class EdmodoOAuth2(BaseOAuth2): - """Edmodo OAuth2""" - name = 'edmodo' - AUTHORIZATION_URL = 'https://api.edmodo.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api.edmodo.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - - def get_user_details(self, response): - """Return user details from Edmodo account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('first_name'), - last_name=response.get('last_name') - ) - return { - 'username': response.get('username'), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from Edmodo""" - return self.get_json( - 'https://api.edmodo.com/users/me', - params={'access_token': access_token} - ) +from social_core.backends.edmodo import EdmodoOAuth2 diff --git a/social/backends/email.py b/social/backends/email.py index b70f1ea67..818ea9ab0 100644 --- a/social/backends/email.py +++ b/social/backends/email.py @@ -1,12 +1 @@ -""" -Legacy Email backend, docs at: - http://psa.matiasaguirre.net/docs/backends/email.html -""" -from social.backends.legacy import LegacyAuth - - -class EmailAuth(LegacyAuth): - name = 'email' - ID_KEY = 'email' - REQUIRES_EMAIL_VALIDATION = True - EXTRA_DATA = ['email'] +from social_core.backends.email import EmailAuth diff --git a/social/backends/eveonline.py b/social/backends/eveonline.py index 1a22ed650..d14b679b3 100644 --- a/social/backends/eveonline.py +++ b/social/backends/eveonline.py @@ -1,41 +1 @@ -""" -EVE Online Single Sign-On (SSO) OAuth2 backend -Documentation at https://developers.eveonline.com/resource/single-sign-on -""" -from social.backends.oauth import BaseOAuth2 - - -class EVEOnlineOAuth2(BaseOAuth2): - """EVE Online OAuth authentication backend""" - name = 'eveonline' - AUTHORIZATION_URL = 'https://login.eveonline.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://login.eveonline.com/oauth/token' - ID_KEY = 'CharacterID' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('CharacterID', 'id'), - ('ExpiresOn', 'expires'), - ('CharacterOwnerHash', 'owner_hash', True), - ('refresh_token', 'refresh_token', True), - ] - - def get_user_details(self, response): - """Return user details from EVE Online account""" - user_data = self.user_data(response['access_token']) - fullname, first_name, last_name = self.get_user_names( - user_data['CharacterName'] - ) - return { - 'email': '', - 'username': fullname, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Get Character data from EVE server""" - return self.get_json( - 'https://login.eveonline.com/oauth/verify', - headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) +from social_core.backends.eveonline import EVEOnlineOAuth2 diff --git a/social/backends/evernote.py b/social/backends/evernote.py index 6a66585e0..fd6a28413 100644 --- a/social/backends/evernote.py +++ b/social/backends/evernote.py @@ -1,75 +1 @@ -""" -Evernote OAuth1 backend (with sandbox mode support), docs at: - http://psa.matiasaguirre.net/docs/backends/evernote.html -""" -from requests import HTTPError - -from social.exceptions import AuthCanceled -from social.backends.oauth import BaseOAuth1 - - -class EvernoteOAuth(BaseOAuth1): - """ - Evernote OAuth authentication backend. - - Possible Values: - {'edam_expires': ['1367525289541'], - 'edam_noteStoreUrl': [ - 'https://sandbox.evernote.com/shard/s1/notestore' - ], - 'edam_shard': ['s1'], - 'edam_userId': ['123841'], - 'edam_webApiUrlPrefix': ['https://sandbox.evernote.com/shard/s1/'], - 'oauth_token': [ - 'S=s1:U=1e3c1:E=13e66dbee45:C=1370f2ac245:P=185:A=my_user:' \ - 'H=411443c5e8b20f8718ed382a19d4ae38' - ]} - """ - name = 'evernote' - ID_KEY = 'edam_userId' - AUTHORIZATION_URL = 'https://www.evernote.com/OAuth.action' - REQUEST_TOKEN_URL = 'https://www.evernote.com/oauth' - ACCESS_TOKEN_URL = 'https://www.evernote.com/oauth' - EXTRA_DATA = [ - ('access_token', 'access_token'), - ('oauth_token', 'oauth_token'), - ('edam_noteStoreUrl', 'store_url'), - ('edam_expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Evernote account""" - return {'username': response['edam_userId'], - 'email': ''} - - def access_token(self, token): - """Return request for access token value""" - try: - return self.get_querystring(self.ACCESS_TOKEN_URL, - auth=self.oauth_auth(token)) - except HTTPError as err: - # Evernote returns a 401 error when AuthCanceled - if err.response.status_code == 401: - raise AuthCanceled(self, response=err.response) - else: - raise - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - data = super(EvernoteOAuth, self).extra_data(user, uid, response, - details, *args, **kwargs) - # Evernote returns expiration timestamp in milliseconds, so it needs to - # be normalized. - if 'expires' in data: - data['expires'] = int(data['expires']) / 1000 - return data - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return access_token.copy() - - -class EvernoteSandboxOAuth(EvernoteOAuth): - name = 'evernote-sandbox' - AUTHORIZATION_URL = 'https://sandbox.evernote.com/OAuth.action' - REQUEST_TOKEN_URL = 'https://sandbox.evernote.com/oauth' - ACCESS_TOKEN_URL = 'https://sandbox.evernote.com/oauth' +from social_core.backends.evernote import EvernoteOAuth, EvernoteSandboxOAuth diff --git a/social/backends/exacttarget.py b/social/backends/exacttarget.py index 7d114becc..a8aefb31d 100644 --- a/social/backends/exacttarget.py +++ b/social/backends/exacttarget.py @@ -1,104 +1 @@ -""" -ExactTarget OAuth support. -Support Authentication from IMH using JWT token and pre-shared key. -Requires package pyjwt -""" -from datetime import timedelta, datetime - -import jwt - -from social.exceptions import AuthFailed, AuthCanceled -from social.backends.oauth import BaseOAuth2 - - -class ExactTargetOAuth2(BaseOAuth2): - name = 'exacttarget' - - def get_user_details(self, response): - """Use the email address of the user, suffixed by _et""" - user = response.get('token', {})\ - .get('request', {})\ - .get('user', {}) - if 'email' in user: - user['username'] = user['email'] - return user - - def get_user_id(self, details, response): - """ - Create a user ID from the ET user ID. Uses details rather than the - default response, as only the token is available in response. details - is much richer: - { - 'expiresIn': 1200, - 'username': 'example@example.com', - 'refreshToken': '1234567890abcdef', - 'internalOauthToken': 'jwttoken.......', - 'oauthToken': 'yetanothertoken', - 'id': 123456, - 'culture': 'en-US', - 'timezone': { - 'shortName': 'CST', - 'offset': -6.0, - 'dst': False, - 'longName': '(GMT-06:00) Central Time (No Daylight Saving)' - }, - 'email': 'example@example.com' - } - """ - return '{0}'.format(details.get('id')) - - def uses_redirect(self): - return False - - def auth_url(self): - return None - - def process_error(self, data): - if data.get('error'): - error = self.data.get('error_description') or self.data['error'] - raise AuthFailed(self, error) - - def do_auth(self, token, *args, **kwargs): - dummy, secret = self.get_key_and_secret() - try: # Decode the token, using the Application Signature from settings - decoded = jwt.decode(token, secret, algorithms=['HS256']) - except jwt.DecodeError: # Wrong signature, fail authentication - raise AuthCanceled(self) - kwargs.update({'response': {'token': decoded}, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - token = self.data.get('jwt', {}) - if not token: - raise AuthFailed(self, 'Authentication Failed') - return self.do_auth(token, *args, **kwargs) - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Load extra details from the JWT token""" - data = { - 'id': details.get('id'), - 'email': details.get('email'), - # OAuth token, for use with legacy SOAP API calls: - # http://bit.ly/13pRHfo - 'internalOauthToken': details.get('internalOauthToken'), - # Token for use with the Application ClientID for the FUEL API - 'oauthToken': details.get('oauthToken'), - # If the token has expired, use the FUEL API to get a new token see - # http://bit.ly/10v1K5l and http://bit.ly/11IbI6F - set legacy=1 - 'refreshToken': details.get('refreshToken'), - } - - # The expiresIn value determines how long the tokens are valid for. - # Take a bit off, then convert to an int timestamp - expiresSeconds = details.get('expiresIn', 0) - 30 - expires = datetime.utcnow() + timedelta(seconds=expiresSeconds) - data['expires'] = (expires - datetime(1970, 1, 1)).total_seconds() - - if response.get('token'): - token = response['token'] - org = token.get('request', {}).get('organization') - if org: - data['stack'] = org.get('stackKey') - data['enterpriseId'] = org.get('enterpriseId') - return data +from social_core.backends.exacttarget import ExactTargetOAuth2 diff --git a/social/backends/facebook.py b/social/backends/facebook.py index f904556c9..611e5d566 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -1,210 +1,2 @@ -""" -Facebook OAuth2 and Canvas Application backends, docs at: - http://psa.matiasaguirre.net/docs/backends/facebook.html -""" -import hmac -import time -import json -import base64 -import hashlib - -from social.utils import parse_qs, constant_time_compare, handle_http_errors -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthException, AuthCanceled, AuthUnknownError, \ - AuthMissingParameter - - -class FacebookOAuth2(BaseOAuth2): - """Facebook OAuth2 authentication backend""" - name = 'facebook' - RESPONSE_TYPE = None - SCOPE_SEPARATOR = ',' - AUTHORIZATION_URL = 'https://www.facebook.com/v2.7/dialog/oauth' - ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.7/oauth/access_token' - REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.7/{uid}/permissions' - REVOKE_TOKEN_METHOD = 'DELETE' - USER_DATA_URL = 'https://graph.facebook.com/v2.7/me' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Facebook account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name', ''), - response.get('first_name', ''), - response.get('last_name', '') - ) - return {'username': response.get('username', response.get('name')), - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - params = self.setting('PROFILE_EXTRA_PARAMS', {}) - params['access_token'] = access_token - - if self.setting('APPSECRET_PROOF', True): - _, secret = self.get_key_and_secret() - params['appsecret_proof'] = hmac.new( - secret.encode('utf8'), - msg=access_token.encode('utf8'), - digestmod=hashlib.sha256 - ).hexdigest() - return self.get_json(self.USER_DATA_URL, params=params) - - def process_error(self, data): - super(FacebookOAuth2, self).process_error(data) - if data.get('error_code'): - raise AuthCanceled(self, data.get('error_message') or - data.get('error_code')) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - self.process_error(self.data) - if not self.data.get('code'): - raise AuthMissingParameter(self, 'code') - state = self.validate_state() - key, secret = self.get_key_and_secret() - response = self.request(self.ACCESS_TOKEN_URL, params={ - 'client_id': key, - 'redirect_uri': self.get_redirect_uri(state), - 'client_secret': secret, - 'code': self.data['code'] - }) - # API v2.3 returns a JSON, according to the documents linked at issue - # #592, but it seems that this needs to be enabled(?), otherwise the - # usual querystring type response is returned. - try: - response = response.json() - except ValueError: - response = parse_qs(response.text) - access_token = response['access_token'] - return self.do_auth(access_token, response, *args, **kwargs) - - def process_refresh_token_response(self, response, *args, **kwargs): - return parse_qs(response.content) - - def refresh_token_params(self, token, *args, **kwargs): - client_id, client_secret = self.get_key_and_secret() - return { - 'fb_exchange_token': token, - 'grant_type': 'fb_exchange_token', - 'client_id': client_id, - 'client_secret': client_secret - } - - def do_auth(self, access_token, response=None, *args, **kwargs): - response = response or {} - - data = self.user_data(access_token) - - if not isinstance(data, dict): - # From time to time Facebook responds back a JSON with just - # False as value, the reason is still unknown, but since the - # data is needed (it contains the user ID used to identify the - # account on further logins), this app cannot allow it to - # continue with the auth process. - raise AuthUnknownError(self, 'An error ocurred while retrieving ' - 'users Facebook data') - - data['access_token'] = access_token - if 'expires' in response: - data['expires'] = response['expires'] - kwargs.update({'backend': self, 'response': data}) - return self.strategy.authenticate(*args, **kwargs) - - def revoke_token_url(self, token, uid): - return self.REVOKE_TOKEN_URL.format(uid=uid) - - def revoke_token_params(self, token, uid): - return {'access_token': token} - - def process_revoke_token_response(self, response): - return super(FacebookOAuth2, self).process_revoke_token_response( - response - ) and response.content == 'true' - - -class FacebookAppOAuth2(FacebookOAuth2): - """Facebook Application Authentication support""" - name = 'facebook-app' - - def uses_redirect(self): - return False - - def auth_complete(self, *args, **kwargs): - access_token = None - response = {} - - if 'signed_request' in self.data: - key, secret = self.get_key_and_secret() - response = self.load_signed_request(self.data['signed_request']) - if 'user_id' not in response and 'oauth_token' not in response: - raise AuthException(self) - - if response is not None: - access_token = response.get('access_token') or \ - response.get('oauth_token') or \ - self.data.get('access_token') - - if access_token is None: - if self.data.get('error') == 'access_denied': - raise AuthCanceled(self) - else: - raise AuthException(self) - return self.do_auth(access_token, response, *args, **kwargs) - - def auth_html(self): - key, secret = self.get_key_and_secret() - namespace = self.setting('NAMESPACE', None) - scope = self.setting('SCOPE', '') - if scope: - scope = self.SCOPE_SEPARATOR.join(scope) - ctx = { - 'FACEBOOK_APP_NAMESPACE': namespace or key, - 'FACEBOOK_KEY': key, - 'FACEBOOK_EXTENDED_PERMISSIONS': scope, - 'FACEBOOK_COMPLETE_URI': self.redirect_uri, - } - tpl = self.setting('LOCAL_HTML', 'facebook.html') - return self.strategy.render_html(tpl=tpl, context=ctx) - - def load_signed_request(self, signed_request): - def base64_url_decode(data): - data = data.encode('ascii') - data += '='.encode('ascii') * (4 - (len(data) % 4)) - return base64.urlsafe_b64decode(data) - - key, secret = self.get_key_and_secret() - try: - sig, payload = signed_request.split('.', 1) - except ValueError: - pass # ignore if can't split on dot - else: - sig = base64_url_decode(sig) - payload_json_bytes = base64_url_decode(payload) - data = json.loads(payload_json_bytes.decode('utf-8', 'replace')) - expected_sig = hmac.new(secret.encode('ascii'), - msg=payload.encode('ascii'), - digestmod=hashlib.sha256).digest() - # allow the signed_request to function for upto 1 day - if constant_time_compare(sig, expected_sig) and \ - data['issued_at'] > (time.time() - 86400): - return data - - -class Facebook2OAuth2(FacebookOAuth2): - """Facebook OAuth2 authentication backend using Facebook Open Graph 2.0""" - AUTHORIZATION_URL = 'https://www.facebook.com/v2.0/dialog/oauth' - ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.0/oauth/access_token' - REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.0/{uid}/permissions' - USER_DATA_URL = 'https://graph.facebook.com/v2.0/me' - - -class Facebook2AppOAuth2(Facebook2OAuth2, FacebookAppOAuth2): - pass +from social_core.backends.facebook import FacebookOAuth2, FacebookAppOAuth2, \ + Facebook2OAuth2, Facebook2AppOAuth2 diff --git a/social/backends/fedora.py b/social/backends/fedora.py index a4b7ff644..00b9c96da 100644 --- a/social/backends/fedora.py +++ b/social/backends/fedora.py @@ -1,11 +1 @@ -""" -Fedora OpenId backend, docs at: - http://psa.matiasaguirre.net/docs/backends/fedora.html -""" -from social.backends.open_id import OpenIdAuth - - -class FedoraOpenId(OpenIdAuth): - name = 'fedora' - URL = 'https://id.fedoraproject.org' - USERNAME_KEY = 'nickname' +from social_core.backends.fedora import FedoraOpenId diff --git a/social/backends/fitbit.py b/social/backends/fitbit.py index 35d9bf1de..6eac17673 100644 --- a/social/backends/fitbit.py +++ b/social/backends/fitbit.py @@ -1,65 +1 @@ -""" -Fitbit OAuth backend, docs at: - http://psa.matiasaguirre.net/docs/backends/fitbit.html -""" -import base64 - -from social.backends.oauth import BaseOAuth1, BaseOAuth2 - - -class FitbitOAuth1(BaseOAuth1): - """Fitbit OAuth1 authentication backend""" - name = 'fitbit' - AUTHORIZATION_URL = 'https://www.fitbit.com/oauth/authorize' - REQUEST_TOKEN_URL = 'https://api.fitbit.com/oauth/request_token' - ACCESS_TOKEN_URL = 'https://api.fitbit.com/oauth/access_token' - ID_KEY = 'encodedId' - EXTRA_DATA = [('encodedId', 'id'), - ('displayName', 'username')] - - def get_user_details(self, response): - """Return user details from Fitbit account""" - return {'username': response.get('displayName'), - 'email': ''} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.fitbit.com/1/user/-/profile.json', - auth=self.oauth_auth(access_token) - )['user'] - -class FitbitOAuth2(BaseOAuth2): - """Fitbit OAuth2 authentication backend""" - name = 'fitbit' - AUTHORIZATION_URL = 'https://www.fitbit.com/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://api.fitbit.com/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REFRESH_TOKEN_URL = 'https://api.fitbit.com/oauth2/token' - DEFAULT_SCOPE = ['profile'] - ID_KEY = 'encodedId' - REDIRECT_STATE = False - EXTRA_DATA = [('expires_in', 'expires'), - ('refresh_token', 'refresh_token', True), - ('encodedId', 'id'), - ('displayName', 'username')] - - def get_user_details(self, response): - """Return user details from Fitbit account""" - return {'username': response.get('displayName'), - 'email': ''} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - auth_header = {"Authorization": "Bearer %s" % access_token} - return self.get_json( - 'https://api.fitbit.com/1/user/-/profile.json', - headers=auth_header - )['user'] - - def auth_headers(self): - return { - 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( - ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) - )) - } +from social_core.backends.fitbit import FitbitOAuth1, FitbitOAuth2 diff --git a/social/backends/flickr.py b/social/backends/flickr.py index b688beb25..8704d6109 100644 --- a/social/backends/flickr.py +++ b/social/backends/flickr.py @@ -1,43 +1 @@ -""" -Flickr OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/flickr.html -""" -from social.backends.oauth import BaseOAuth1 - - -class FlickrOAuth(BaseOAuth1): - """Flickr OAuth authentication backend""" - name = 'flickr' - AUTHORIZATION_URL = 'https://www.flickr.com/services/oauth/authorize' - REQUEST_TOKEN_URL = 'https://www.flickr.com/services/oauth/request_token' - ACCESS_TOKEN_URL = 'https://www.flickr.com/services/oauth/access_token' - EXTRA_DATA = [ - ('id', 'id'), - ('username', 'username'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Flickr account""" - fullname, first_name, last_name = self.get_user_names( - response.get('fullname') - ) - return {'username': response.get('username') or response.get('id'), - 'email': '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return { - 'id': access_token['user_nsid'], - 'username': access_token['username'], - 'fullname': access_token.get('fullname', ''), - } - - def auth_extra_arguments(self): - params = super(FlickrOAuth, self).auth_extra_arguments() or {} - if 'perms' not in params: - params['perms'] = 'read' - return params +from social_core.backends.flickr import FlickrOAuth diff --git a/social/backends/foursquare.py b/social/backends/foursquare.py index f6a5fba08..51636d3a2 100644 --- a/social/backends/foursquare.py +++ b/social/backends/foursquare.py @@ -1,36 +1 @@ -""" -Foursquare OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/foursquare.html -""" -from social.backends.oauth import BaseOAuth2 - - -class FoursquareOAuth2(BaseOAuth2): - name = 'foursquare' - AUTHORIZATION_URL = 'https://foursquare.com/oauth2/authenticate' - ACCESS_TOKEN_URL = 'https://foursquare.com/oauth2/access_token' - ACCESS_TOKEN_METHOD = 'POST' - API_VERSION = '20140128' - - def get_user_id(self, details, response): - return response['response']['user']['id'] - - def get_user_details(self, response): - """Return user details from Foursquare account""" - info = response['response']['user'] - email = info['contact']['email'] - fullname, first_name, last_name = self.get_user_names( - first_name=info.get('firstName', ''), - last_name=info.get('lastName', '') - ) - return {'username': first_name + ' ' + last_name, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.foursquare.com/v2/users/self', - params={'oauth_token': access_token, - 'v': self.API_VERSION}) +from social_core.backends.foursquare import FoursquareOAuth2 diff --git a/social/backends/gae.py b/social/backends/gae.py index 833e3d83a..77398ca87 100644 --- a/social/backends/gae.py +++ b/social/backends/gae.py @@ -1,40 +1 @@ -""" -Google App Engine support using User API -""" -from __future__ import absolute_import - -from google.appengine.api import users - -from social.backends.base import BaseAuth -from social.exceptions import AuthException - - -class GoogleAppEngineAuth(BaseAuth): - """GoogleAppengine authentication backend""" - name = 'google-appengine' - - def get_user_id(self, details, response): - """Return current user id.""" - user = users.get_current_user() - if user: - return user.user_id() - - def get_user_details(self, response): - """Return user basic information (id and email only).""" - user = users.get_current_user() - return {'username': user.user_id(), - 'email': user.email(), - 'fullname': '', - 'first_name': '', - 'last_name': ''} - - def auth_url(self): - """Build and return complete URL.""" - return users.create_login_url(self.redirect_uri) - - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance.""" - if not users.get_current_user(): - raise AuthException('Authentication error') - kwargs.update({'response': '', 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) +from social_core.backends.gae import GoogleAppEngineAuth diff --git a/social/backends/github.py b/social/backends/github.py index f5d875644..6453df072 100644 --- a/social/backends/github.py +++ b/social/backends/github.py @@ -1,119 +1,2 @@ -""" -Github OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/github.html -""" -from requests import HTTPError - -from six.moves.urllib.parse import urljoin - -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthFailed - - -class GithubOAuth2(BaseOAuth2): - """Github OAuth authentication backend""" - name = 'github' - API_URL = 'https://api.github.com/' - AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize' - ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires'), - ('login', 'login') - ] - - def api_url(self): - return self.API_URL - - def get_user_details(self, response): - """Return user details from Github account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - return {'username': response.get('login'), - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - data = self._user_data(access_token) - if not data.get('email'): - try: - emails = self._user_data(access_token, '/emails') - except (HTTPError, ValueError, TypeError): - emails = [] - - if emails: - email = emails[0] - primary_emails = [ - e for e in emails - if not isinstance(e, dict) or e.get('primary') - ] - if primary_emails: - email = primary_emails[0] - if isinstance(email, dict): - email = email.get('email', '') - data['email'] = email - return data - - def _user_data(self, access_token, path=None): - url = urljoin(self.api_url(), 'user{0}'.format(path or '')) - return self.get_json(url, params={'access_token': access_token}) - - -class GithubMemberOAuth2(GithubOAuth2): - no_member_string = '' - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - user_data = super(GithubMemberOAuth2, self).user_data( - access_token, *args, **kwargs - ) - try: - self.request(self.member_url(user_data), params={ - 'access_token': access_token - }) - except HTTPError as err: - # if the user is a member of the organization, response code - # will be 204, see http://bit.ly/ZS6vFl - if err.response.status_code != 204: - raise AuthFailed(self, - 'User doesn\'t belong to the organization') - return user_data - - def member_url(self, user_data): - raise NotImplementedError('Implement in subclass') - - -class GithubOrganizationOAuth2(GithubMemberOAuth2): - """Github OAuth2 authentication backend for organizations""" - name = 'github-org' - no_member_string = 'User doesn\'t belong to the organization' - - def member_url(self, user_data): - return urljoin( - self.api_url(), - 'orgs/{org}/members/{username}'.format( - org=self.setting('NAME'), - username=user_data.get('login') - ) - ) - - -class GithubTeamOAuth2(GithubMemberOAuth2): - """Github OAuth2 authentication backend for teams""" - name = 'github-team' - no_member_string = 'User doesn\'t belong to the team' - - def member_url(self, user_data): - return urljoin( - self.api_url(), - 'teams/{team_id}/members/{username}'.format( - team_id=self.setting('ID'), - username=user_data.get('login') - ) - ) +from social_core.backends.github import GithubOAuth2, GithubMemberOAuth2, \ + GithubOrganizationOAuth2, GithubTeamOAuth2 diff --git a/social/backends/github_enterprise.py b/social/backends/github_enterprise.py index 656a92e6d..be1ec44c2 100644 --- a/social/backends/github_enterprise.py +++ b/social/backends/github_enterprise.py @@ -1,42 +1,3 @@ -""" -Github Enterprise OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/github_enterprise.html -""" -from six.moves.urllib.parse import urljoin - -from social.utils import append_slash -from social.backends.github import GithubOAuth2, GithubOrganizationOAuth2, \ - GithubTeamOAuth2 - - -class GithubEnterpriseMixin(object): - def api_url(self): - return append_slash(self.setting('API_URL')) - - def authorization_url(self): - return self._url('login/oauth/authorize') - - def access_token_url(self): - return self._url('login/oauth/access_token') - - def _url(self, path): - return urljoin(append_slash(self.setting('URL')), path) - - -class GithubEnterpriseOAuth2(GithubEnterpriseMixin, GithubOAuth2): - """Github Enterprise OAuth authentication backend""" - name = 'github-enterprise' - - -class GithubEnterpriseOrganizationOAuth2(GithubEnterpriseMixin, - GithubOrganizationOAuth2): - """Github Enterprise OAuth2 authentication backend for - organizations""" - name = 'github-enterprise-org' - DEFAULT_SCOPE = ['read:org'] - - -class GithubEnterpriseTeamOAuth2(GithubEnterpriseMixin, GithubTeamOAuth2): - """Github Enterprise OAuth2 authentication backend for teams""" - name = 'github-enterprise-team' - DEFAULT_SCOPE = ['read:org'] +from social_core.backends.github_enterprise import GithubEnterpriseMixin, \ + GithubEnterpriseOAuth2, GithubEnterpriseOrganizationOAuth2, \ + GithubEnterpriseTeamOAuth2 diff --git a/social/backends/goclio.py b/social/backends/goclio.py index 7da0eedf8..2f5b2ee55 100644 --- a/social/backends/goclio.py +++ b/social/backends/goclio.py @@ -1,35 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class GoClioOAuth2(BaseOAuth2): - name = 'goclio' - AUTHORIZATION_URL = 'https://app.goclio.com/oauth/authorize/' - ACCESS_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_URL = 'https://app.goclio.com/oauth/token/' - REDIRECT_STATE = False - STATE_PARAMETER = False - - def get_user_details(self, response): - """Return user details from GoClio account""" - user = response.get('user', {}) - username = user.get('id', None) - email = user.get('email', None) - first_name, last_name = (user.get('first_name', None), - user.get('last_name', None)) - fullname = '%s %s' % (first_name, last_name) - - return {'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://app.goclio.com/api/v2/users/who_am_i', - params={'access_token': access_token} - ) - - def get_user_id(self, details, response): - return response.get('user', {}).get('id') +from social_core.backends.goclio import GoClioOAuth2 diff --git a/social/backends/goclioeu.py b/social/backends/goclioeu.py index f5b36c46b..d79f2d49c 100644 --- a/social/backends/goclioeu.py +++ b/social/backends/goclioeu.py @@ -1,14 +1 @@ -from social.backends.goclio import GoClioOAuth2 - - -class GoClioEuOAuth2(GoClioOAuth2): - name = 'goclioeu' - AUTHORIZATION_URL = 'https://app.goclio.eu/oauth/authorize/' - ACCESS_TOKEN_URL = 'https://app.goclio.eu/oauth/token/' - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://app.goclio.eu/api/v2/users/who_am_i', - params={'access_token': access_token} - ) +from social_core.backends.goclioeu import GoClioEuOAuth2 diff --git a/social/backends/google.py b/social/backends/google.py index 8affe6d2f..32636a4c7 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -1,212 +1,3 @@ -""" -Google OpenId, OAuth2, OAuth1, Google+ Sign-in backends, docs at: - http://psa.matiasaguirre.net/docs/backends/google.html -""" -from social.utils import handle_http_errors -from social.backends.open_id import OpenIdAuth, OpenIdConnectAuth -from social.backends.oauth import BaseOAuth2, BaseOAuth1 -from social.exceptions import AuthMissingParameter - - -class BaseGoogleAuth(object): - def get_user_id(self, details, response): - """Use google email as unique id""" - if self.setting('USE_UNIQUE_USER_ID', False): - return response['id'] - else: - return details['email'] - - def get_user_details(self, response): - """Return user details from Google API account""" - if 'email' in response: - email = response['email'] - elif 'emails' in response: - email = response['emails'][0]['value'] - else: - email = '' - - if isinstance(response.get('name'), dict): - names = response.get('name') or {} - name, given_name, family_name = ( - response.get('displayName', ''), - names.get('givenName', ''), - names.get('familyName', '') - ) - else: - name, given_name, family_name = ( - response.get('name', ''), - response.get('given_name', ''), - response.get('family_name', '') - ) - - fullname, first_name, last_name = self.get_user_names( - name, given_name, family_name - ) - return {'username': email.split('@', 1)[0], - 'email': email, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - -class BaseGoogleOAuth2API(BaseGoogleAuth): - def get_scope(self): - """Return list with needed access scope""" - scope = self.setting('SCOPE', []) - if not self.setting('IGNORE_DEFAULT_SCOPE', False): - default_scope = [] - if self.setting('USE_DEPRECATED_API', False): - default_scope = self.DEPRECATED_DEFAULT_SCOPE - else: - default_scope = self.DEFAULT_SCOPE - scope = scope + (default_scope or []) - return scope - - def user_data(self, access_token, *args, **kwargs): - """Return user data from Google API""" - if self.setting('USE_DEPRECATED_API', False): - url = 'https://www.googleapis.com/oauth2/v1/userinfo' - else: - url = 'https://www.googleapis.com/plus/v1/people/me' - return self.get_json(url, params={ - 'access_token': access_token, - 'alt': 'json' - }) - - def revoke_token_params(self, token, uid): - return {'token': token} - - def revoke_token_headers(self, token, uid): - return {'Content-type': 'application/json'} - - -class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): - """Google OAuth2 authentication backend""" - name = 'google-oauth2' - REDIRECT_STATE = False - AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth' - ACCESS_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' - REVOKE_TOKEN_METHOD = 'GET' - # The order of the default scope is important - DEFAULT_SCOPE = ['openid', 'email', 'profile'] - DEPRECATED_DEFAULT_SCOPE = [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile' - ] - EXTRA_DATA = [ - ('refresh_token', 'refresh_token', True), - ('expires_in', 'expires'), - ('token_type', 'token_type', True) - ] - - -class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): - name = 'google-plus' - REDIRECT_STATE = False - STATE_PARAMETER = False - AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth' - ACCESS_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke' - REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = [ - 'https://www.googleapis.com/auth/plus.login', - 'https://www.googleapis.com/auth/plus.me', - ] - DEPRECATED_DEFAULT_SCOPE = [ - 'https://www.googleapis.com/auth/plus.login', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile' - ] - EXTRA_DATA = [ - ('id', 'user_id'), - ('refresh_token', 'refresh_token', True), - ('expires_in', 'expires'), - ('access_type', 'access_type', True), - ('code', 'code') - ] - - def auth_complete_params(self, state=None): - params = super(GooglePlusAuth, self).auth_complete_params(state) - if self.data.get('access_token'): - # Don't add postmessage if this is plain server-side workflow - params['redirect_uri'] = 'postmessage' - return params - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - if 'access_token' in self.data: # Client-side workflow - token = self.data.get('access_token') - response = self.get_json( - 'https://www.googleapis.com/oauth2/v1/tokeninfo', - params={'access_token': token} - ) - self.process_error(response) - return self.do_auth(token, response=response, *args, **kwargs) - elif 'code' in self.data: # Server-side workflow - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - return self.do_auth(response['access_token'], - response=response, - *args, **kwargs) - else: - raise AuthMissingParameter(self, 'access_token or code') - - -class GoogleOAuth(BaseGoogleAuth, BaseOAuth1): - """Google OAuth authorization mechanism""" - name = 'google-oauth' - AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken' - REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken' - ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken' - DEFAULT_SCOPE = ['https://www.googleapis.com/auth/userinfo#email'] - - def user_data(self, access_token, *args, **kwargs): - """Return user data from Google API""" - return self.get_querystring( - 'https://www.googleapis.com/userinfo/email', - auth=self.oauth_auth(access_token) - ) - - def get_key_and_secret(self): - """Return Google OAuth Consumer Key and Consumer Secret pair, uses - anonymous by default, beware that this marks the application as not - registered and a security badge is displayed on authorization page. - http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth - """ - key_secret = super(GoogleOAuth, self).get_key_and_secret() - if key_secret == (None, None): - key_secret = ('anonymous', 'anonymous') - return key_secret - - -class GoogleOpenId(OpenIdAuth): - name = 'google' - URL = 'https://www.google.com/accounts/o8/id' - - def get_user_id(self, details, response): - """ - Return user unique id provided by service. For google user email - is unique enought to flag a single user. Email comes from schema: - http://axschema.org/contact/email - """ - return details['email'] - - -class GoogleOpenIdConnect(GoogleOAuth2, OpenIdConnectAuth): - name = 'google-openidconnect' - ID_TOKEN_ISSUER = "accounts.google.com" - - def user_data(self, access_token, *args, **kwargs): - """Return user data from Google API""" - return self.get_json( - 'https://www.googleapis.com/plus/v1/people/me/openIdConnect', - params={'access_token': access_token, 'alt': 'json'} - ) +from social_core.backends.google import BaseGoogleAuth, BaseGoogleOAuth2API, \ + GoogleOAuth2, GooglePlusAuth, GoogleOAuth, GoogleOpenId, \ + GoogleOpenIdConnect diff --git a/social/backends/instagram.py b/social/backends/instagram.py index 494d8e2a0..780ba35ee 100644 --- a/social/backends/instagram.py +++ b/social/backends/instagram.py @@ -1,53 +1 @@ -""" -Instagram OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/instagram.html -""" -import hmac - -from hashlib import sha256 - -from social.backends.oauth import BaseOAuth2 - - -class InstagramOAuth2(BaseOAuth2): - name = 'instagram' - AUTHORIZATION_URL = 'https://api.instagram.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api.instagram.com/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - - def get_user_id(self, details, response): - # Sometimes Instagram returns 'user', sometimes 'data', but API docs - # says 'data' http://instagram.com/developer/endpoints/users/#get_users - user = response.get('user') or response.get('data') or {} - return user.get('id') - - def get_user_details(self, response): - """Return user details from Instagram account""" - # Sometimes Instagram returns 'user', sometimes 'data', but API docs - # says 'data' http://instagram.com/developer/endpoints/users/#get_users - user = response.get('user') or response.get('data') or {} - username = user['username'] - email = user.get('email', '') - fullname, first_name, last_name = self.get_user_names( - user.get('full_name', '') - ) - return {'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - key, secret = self.get_key_and_secret() - params = {'access_token': access_token} - sig = self._generate_sig("/users/self", params, secret) - params['sig'] = sig - return self.get_json('https://api.instagram.com/v1/users/self', - params=params) - - def _generate_sig(self, endpoint, params, secret): - sig = endpoint - for key in sorted(params.keys()): - sig += '|%s=%s' % (key, params[key]) - return hmac.new(secret.encode(), sig.encode(), sha256).hexdigest() +from social_core.backends.instagram import InstagramOAuth2 diff --git a/social/backends/itembase.py b/social/backends/itembase.py index 8419f38b4..3e3ba1f98 100644 --- a/social/backends/itembase.py +++ b/social/backends/itembase.py @@ -1,85 +1 @@ -import time - -from social.backends.oauth import BaseOAuth2 -from social.utils import handle_http_errors - - -class ItembaseOAuth2(BaseOAuth2): - name = 'itembase' - ID_KEY = 'uuid' - AUTHORIZATION_URL = 'https://accounts.itembase.com/oauth/v2/auth' - ACCESS_TOKEN_URL = 'https://accounts.itembase.com/oauth/v2/token' - USER_DETAILS_URL = 'https://users.itembase.com/v1/me' - ACTIVATION_ENDPOINT = 'https://solutionservice.itembase.com/activate' - DEFAULT_SCOPE = ['user.minimal'] - EXTRA_DATA = [ - ('access_token', 'access_token'), - ('token_type', 'token_type'), - ('refresh_token', 'refresh_token'), - ('expires_in', 'expires_in'), # seconds to expiration - ('expires', 'expires'), # expiration timestamp in UTC - ('uuid', 'uuid'), - ('username', 'username'), - ('email', 'email'), - ('first_name', 'first_name'), - ('middle_name', 'middle_name'), - ('last_name', 'last_name'), - ('name_format', 'name_format'), - ('locale', 'locale'), - ('preferred_currency', 'preferred_currency'), - ] - - def add_expires(self, data): - data['expires'] = int(time.time()) + data.get('expires_in', 0) - return data - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - data = BaseOAuth2.extra_data(self, user, uid, response, details=details, - *args, **kwargs) - return self.add_expires(data) - - def process_refresh_token_response(self, response, *args, **kwargs): - data = BaseOAuth2.process_refresh_token_response(self, response, - *args, **kwargs) - return self.add_expires(data) - - def get_user_details(self, response): - """Return user details from Itembase account""" - return response - - def user_data(self, access_token, *args, **kwargs): - return self.get_json(self.USER_DETAILS_URL, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) - }) - - def activation_data(self, response): - # returns activation_data dict with activation_url inside - # see http://developers.itembase.com/authentication/activation - return self.get_json(self.ACTIVATION_ENDPOINT, headers={ - 'Authorization': 'Bearer {0}'.format(response['access_token']) - }) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - state = self.validate_state() - self.process_error(self.data) - # itembase needs GET request with params instead of just data - response = self.request_access_token( - self.access_token_url(), - params=self.auth_complete_params(state), - headers=self.auth_headers(), - auth=self.auth_complete_credentials(), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) - - -class ItembaseOAuth2Sandbox(ItembaseOAuth2): - name = 'itembase-sandbox' - AUTHORIZATION_URL = 'http://sandbox.accounts.itembase.io/oauth/v2/auth' - ACCESS_TOKEN_URL = 'http://sandbox.accounts.itembase.io/oauth/v2/token' - USER_DETAILS_URL = 'http://sandbox.users.itembase.io/v1/me' - ACTIVATION_ENDPOINT = 'http://sandbox.solutionservice.itembase.io/activate' +from social_core.backends.itembase import ItembaseOAuth2, ItembaseOAuth2Sandbox diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index 52c84ce80..7c3138e1d 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -1,77 +1 @@ -""" -Jawbone OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/jawbone.html -""" -from social.utils import handle_http_errors -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthCanceled, AuthUnknownError - - -class JawboneOAuth2(BaseOAuth2): - name = 'jawbone' - AUTHORIZATION_URL = 'https://jawbone.com/auth/oauth2/auth' - ACCESS_TOKEN_URL = 'https://jawbone.com/auth/oauth2/token' - SCOPE_SEPARATOR = ' ' - REDIRECT_STATE = False - - def get_user_id(self, details, response): - return response['data']['xid'] - - def get_user_details(self, response): - """Return user details from Jawbone account""" - data = response['data'] - fullname, first_name, last_name = self.get_user_names( - first_name=data.get('first', ''), - last_name=data.get('last', '') - ) - return { - 'username': first_name + ' ' + last_name, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'dob': data.get('dob', ''), - 'gender': data.get('gender', ''), - 'height': data.get('height', ''), - 'weight': data.get('weight', '') - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://jawbone.com/nudge/api/users/@me', - headers={'Authorization': 'Bearer ' + access_token}, - ) - - def process_error(self, data): - error = data.get('error') - if error: - if error == 'access_denied': - raise AuthCanceled(self) - else: - raise AuthUnknownError(self, 'Jawbone error was {0}'.format( - error - )) - return super(JawboneOAuth2, self).process_error(data) - - def auth_complete_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'client_id': client_id, - 'client_secret': client_secret, - } - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - self.process_error(self.data) - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - params=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) +from social_core.backends.jawbone import JawboneOAuth2 diff --git a/social/backends/justgiving.py b/social/backends/justgiving.py index e5ced3eba..7549b696c 100644 --- a/social/backends/justgiving.py +++ b/social/backends/justgiving.py @@ -1,56 +1 @@ -from requests.auth import HTTPBasicAuth -from social.utils import handle_http_errors -from social.backends.oauth import BaseOAuth2 - - -class JustGivingOAuth2(BaseOAuth2): - """Just Giving OAuth authentication backend""" - name = 'justgiving' - ID_KEY = 'userId' - AUTHORIZATION_URL = 'https://identity.justgiving.com/connect/authorize' - ACCESS_TOKEN_URL = 'https://identity.justgiving.com/connect/token' - ACCESS_TOKEN_METHOD = 'POST' - USER_DATA_URL = 'https://api.justgiving.com/v1/account' - DEFAULT_SCOPE = ['openid', 'account', 'profile', 'email', 'fundraise'] - - def get_user_details(self, response): - """Return user details from Just Giving account""" - fullname, first_name, last_name = self.get_user_names( - '', - response.get('firstName'), - response.get('lastName')) - return { - 'username': response.get('email'), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - key, secret = self.get_key_and_secret() - return self.get_json(self.USER_DATA_URL, headers={ - 'Authorization': 'Bearer {0}'.format(access_token), - 'Content-Type': 'application/json', - 'x-application-key': secret, - 'x-api-key': key - }) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - state = self.validate_state() - self.process_error(self.data) - - key, secret = self.get_key_and_secret() - response = self.request_access_token( - self.access_token_url(), - data=self.auth_complete_params(state), - headers=self.auth_headers(), - auth=HTTPBasicAuth(key, secret), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) +from social_core.backends.justgiving import JustGivingOAuth2 diff --git a/social/backends/kakao.py b/social/backends/kakao.py index 279a5aa1e..75c1a1f68 100644 --- a/social/backends/kakao.py +++ b/social/backends/kakao.py @@ -1,40 +1 @@ -""" -Kakao OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/kakao.html -""" -from social.backends.oauth import BaseOAuth2 - - -class KakaoOAuth2(BaseOAuth2): - """Kakao OAuth authentication backend""" - name = 'kakao' - AUTHORIZATION_URL = 'https://kauth.kakao.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://kauth.kakao.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def get_user_id(self, details, response): - return response['id'] - - def get_user_details(self, response): - """Return user details from Kakao account""" - nickname = response['properties']['nickname'] - return { - 'username': nickname, - 'email': '', - 'fullname': '', - 'first_name': '', - 'last_name': '' - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://kapi.kakao.com/v1/user/me', - params={'access_token': access_token}) - - def auth_complete_params(self, state=None): - return { - 'grant_type': 'authorization_code', - 'code': self.data.get('code', ''), - 'client_id': self.get_key_and_secret()[0], - } +from social_core.backends.kakao import KakaoOAuth2 diff --git a/social/backends/khanacademy.py b/social/backends/khanacademy.py index 891292bce..7ae755869 100644 --- a/social/backends/khanacademy.py +++ b/social/backends/khanacademy.py @@ -1,125 +1,2 @@ -""" -Khan Academy OAuth backend, docs at: - https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication -""" -import six - -from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_TYPE_QUERY -from requests_oauthlib import OAuth1 - -from social.backends.oauth import BaseOAuth1 -from social.p3 import urlencode - - -class BrowserBasedOAuth1(BaseOAuth1): - """Browser based mechanism OAuth authentication, fill the needed - parameters to communicate properly with authentication service. - - REQUEST_TOKEN_URL Request token URL (opened in web browser) - ACCESS_TOKEN_URL Access token URL - """ - REQUEST_TOKEN_URL = '' - OAUTH_TOKEN_PARAMETER_NAME = 'oauth_token' - REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' - ACCESS_TOKEN_URL = '' - - def auth_url(self): - """Return redirect url""" - return self.unauthorized_token_request() - - def get_unauthorized_token(self): - return self.strategy.request_data() - - def unauthorized_token_request(self): - """Return request for unauthorized token (first stage)""" - - params = self.request_token_extra_arguments() - params.update(self.get_scope_argument()) - key, secret = self.get_key_and_secret() - # decoding='utf-8' produces errors with python-requests on Python3 - # since the final URL will be of type bytes - decoding = None if six.PY3 else 'utf-8' - state = self.get_or_create_state() - auth = OAuth1( - key, - secret, - callback_uri=self.get_redirect_uri(state), - decoding=decoding, - signature_method=SIGNATURE_HMAC, - signature_type=SIGNATURE_TYPE_QUERY - ) - url = self.REQUEST_TOKEN_URL + '?' + urlencode(params) - url, _, _ = auth.client.sign(url) - return url - - def oauth_auth(self, token=None, oauth_verifier=None): - key, secret = self.get_key_and_secret() - oauth_verifier = oauth_verifier or self.data.get('oauth_verifier') - token = token or {} - # decoding='utf-8' produces errors with python-requests on Python3 - # since the final URL will be of type bytes - decoding = None if six.PY3 else 'utf-8' - state = self.get_or_create_state() - return OAuth1(key, secret, - resource_owner_key=token.get('oauth_token'), - resource_owner_secret=token.get('oauth_token_secret'), - callback_uri=self.get_redirect_uri(state), - verifier=oauth_verifier, - signature_method=SIGNATURE_HMAC, - signature_type=SIGNATURE_TYPE_QUERY, - decoding=decoding) - - -class KhanAcademyOAuth1(BrowserBasedOAuth1): - """ - Class used for autorising with Khan Academy. - - Flow of Khan Academy is a bit different than most OAuth 1.0 and consinsts - of the following steps: - 1. Create signed params to attach to the REQUEST_TOKEN_URL - 2. Redirect user to the REQUEST_TOKEN_URL that will respond with - oauth_secret, oauth_token, oauth_verifier that should be used with - ACCESS_TOKEN_URL - 3. Go to ACCESS_TOKEN_URL and grab oauth_token_secret. - - Note that we don't use the AUTHORIZATION_URL. - - REQUEST_TOKEN_URL requires the following arguments: - oauth_consumer_key - Your app's consumer key - oauth_nonce - Random 64-bit, unsigned number encoded as an ASCII string - in decimal format. The nonce/timestamp pair should always be unique. - oauth_version - OAuth version used by your app. Must be "1.0" for now. - oauth_signature - String generated using the referenced signature method. - oauth_signature_method - Signature algorithm (currently only support - "HMAC-SHA1") - oauth_timestamp - Integer representing the time the request is sent. - The timestamp should be expressed in number of seconds - after January 1, 1970 00:00:00 GMT. - oauth_callback (optional) - URL to redirect to after request token is - received and authorized by the user's chosen identity provider. - """ - name = 'khanacademy-oauth1' - ID_KEY = 'user_id' - REQUEST_TOKEN_URL = 'http://www.khanacademy.org/api/auth/request_token' - ACCESS_TOKEN_URL = 'https://www.khanacademy.org/api/auth/access_token' - REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' - USER_DATA_URL = 'https://www.khanacademy.org/api/v1/user' - - EXTRA_DATA = [('user_id', 'user_id')] - - def get_user_details(self, response): - """Return user details from Khan Academy account""" - return { - 'username': response.get('key_email'), - 'email': response.get('key_email'), - 'fullname': '', - 'first_name': '', - 'last_name': '', - 'user_id': response.get('user_id') - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - auth = self.oauth_auth(access_token) - url, _, _ = auth.client.sign(self.USER_DATA_URL) - return self.get_json(url) +from social_core.backends.khanacademy import BrowserBasedOAuth1, \ + KhanAcademyOAuth1 diff --git a/social/backends/lastfm.py b/social/backends/lastfm.py index 0f1acf9db..6851008d6 100644 --- a/social/backends/lastfm.py +++ b/social/backends/lastfm.py @@ -1,59 +1 @@ -import hashlib - -from social.utils import handle_http_errors -from social.backends.base import BaseAuth - - -class LastFmAuth(BaseAuth): - """ - Last.Fm authentication backend. Requires two settings: - SOCIAL_AUTH_LASTFM_KEY - SOCIAL_AUTH_LASTFM_SECRET - - Don't forget to set the Last.fm callback to something sensible like - http://your.site/lastfm/complete - """ - name = 'lastfm' - AUTH_URL = 'http://www.last.fm/api/auth/?api_key={api_key}' - EXTRA_DATA = [ - ('key', 'session_key') - ] - - def auth_url(self): - return self.AUTH_URL.format(api_key=self.setting('KEY')) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - key, secret = self.get_key_and_secret() - token = self.data['token'] - - signature = hashlib.md5(''.join( - ('api_key', key, 'methodauth.getSession', 'token', token, secret) - ).encode()).hexdigest() - - response = self.get_json('http://ws.audioscrobbler.com/2.0/', data={ - 'method': 'auth.getSession', - 'api_key': key, - 'token': token, - 'api_sig': signature, - 'format': 'json' - }, method='POST') - - kwargs.update({'response': response['session'], 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def get_user_id(self, details, response): - """Return a unique ID for the current user, by default from server - response.""" - return response.get('name') - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names(response['name']) - return { - 'username': response['name'], - 'email': '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } +from social_core.backends.lastfm import LastFmAuth diff --git a/social/backends/launchpad.py b/social/backends/launchpad.py index 2f4a51390..a7bd229dd 100644 --- a/social/backends/launchpad.py +++ b/social/backends/launchpad.py @@ -1,11 +1 @@ -""" -Launchpad OpenId backend -""" - -from social.backends.open_id import OpenIdAuth - - -class LaunchpadOpenId(OpenIdAuth): - name = 'launchpad' - URL = 'https://login.launchpad.net' - USERNAME_KEY = 'nickname' +from social_core.backends.launchpad import LaunchpadOpenId diff --git a/social/backends/legacy.py b/social/backends/legacy.py index 0ea474060..f686fab09 100644 --- a/social/backends/legacy.py +++ b/social/backends/legacy.py @@ -1,44 +1 @@ -from social.backends.base import BaseAuth -from social.exceptions import AuthMissingParameter - - -class LegacyAuth(BaseAuth): - def get_user_id(self, details, response): - return details.get(self.ID_KEY) or \ - response.get(self.ID_KEY) - - def auth_url(self): - return self.setting('FORM_URL') - - def auth_html(self): - return self.strategy.render_html(tpl=self.setting('FORM_HTML')) - - def uses_redirect(self): - return self.setting('FORM_URL') and not \ - self.setting('FORM_HTML') - - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - if self.ID_KEY not in self.data: - raise AuthMissingParameter(self, self.ID_KEY) - kwargs.update({'response': self.data, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def get_user_details(self, response): - """Return user details""" - email = response.get('email', '') - username = response.get('username', '') - fullname, first_name, last_name = self.get_user_names( - response.get('fullname', ''), - response.get('first_name', ''), - response.get('last_name', '') - ) - if email and not username: - username = email.split('@', 1)[0] - return { - 'username': username, - 'email': email, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } +from social_core.backends.legacy import LegacyAuth diff --git a/social/backends/line.py b/social/backends/line.py index ba8136ed8..94543849f 100644 --- a/social/backends/line.py +++ b/social/backends/line.py @@ -1,91 +1 @@ -# vim:fileencoding=utf-8 -import requests -import json - -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthFailed -from social.utils import handle_http_errors - - -class LineOAuth2(BaseOAuth2): - name = 'line' - AUTHORIZATION_URL = 'https://access.line.me/dialog/oauth/weblogin' - ACCESS_TOKEN_URL = 'https://api.line.me/v1/oauth/accessToken' - BASE_API_URL = 'https://api.line.me' - USER_INFO_URL = BASE_API_URL + '/v1/profile' - ACCESS_TOKEN_METHOD = 'POST' - STATE_PARAMETER = True - REDIRECT_STATE = True - ID_KEY = 'mid' - EXTRA_DATA = [ - ('mid', 'id'), - ('expire', 'expire'), - ('refreshToken', 'refresh_token') - ] - - def auth_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - return { - 'client_id': client_id, - 'redirect_uri': self.get_redirect_uri(), - 'response_type': self.RESPONSE_TYPE - } - - def process_error(self, data): - error_code = data.get('errorCode') or \ - data.get('statusCode') or \ - data.get('error') - error_message = data.get('errorMessage') or \ - data.get('statusMessage') or \ - data.get('error_desciption') - if error_code is not None or error_message is not None: - raise AuthFailed(self, error_message or error_code) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - client_id, client_secret = self.get_key_and_secret() - code = self.data.get('code') - - self.process_error(self.data) - - try: - response = self.request_access_token( - self.access_token_url(), - method=self.ACCESS_TOKEN_METHOD, - params={ - 'requestToken': code, - 'channelSecret': client_secret - } - ) - self.process_error(response) - - return self.do_auth(response['accessToken'], response=response, - *args, **kwargs) - except requests.HTTPError as err: - self.process_error(json.loads(err.response.content)) - - def get_user_details(self, response): - response.update({ - 'fullname': response.get('displayName'), - 'picture_url': response.get('pictureUrl') - }) - return response - - def get_user_id(self, details, response): - """Return a unique ID for the current user, by default from server response.""" - return response.get(self.ID_KEY) - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - try: - response = self.get_json( - self.USER_INFO_URL, - headers={ - "Authorization": "Bearer {}".format(access_token) - } - ) - self.process_error(response) - return response - except requests.HTTPError as err: - self.process_error(err.response.json()) +from social_core.backends.line import LineOAuth2 diff --git a/social/backends/linkedin.py b/social/backends/linkedin.py index 739e18176..91975b796 100644 --- a/social/backends/linkedin.py +++ b/social/backends/linkedin.py @@ -1,96 +1,2 @@ -""" -LinkedIn OAuth1 and OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/linkedin.html -""" -from social.backends.oauth import BaseOAuth1, BaseOAuth2 - - -class BaseLinkedinAuth(object): - EXTRA_DATA = [('id', 'id'), - ('first-name', 'first_name', True), - ('last-name', 'last_name', True), - ('firstName', 'first_name', True), - ('lastName', 'last_name', True)] - USER_DETAILS = 'https://api.linkedin.com/v1/people/~:({0})' - - def get_user_details(self, response): - """Return user details from Linkedin account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response['firstName'], - last_name=response['lastName'] - ) - email = response.get('emailAddress', '') - return {'username': first_name + last_name, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_details_url(self): - # use set() since LinkedIn fails when values are duplicated - fields_selectors = list(set(['first-name', 'id', 'last-name'] + - self.setting('FIELD_SELECTORS', []))) - # user sort to ease the tests URL mocking - fields_selectors.sort() - fields_selectors = ','.join(fields_selectors) - return self.USER_DETAILS.format(fields_selectors) - - def user_data_headers(self): - lang = self.setting('FORCE_PROFILE_LANGUAGE') - if lang: - return { - 'Accept-Language': lang if lang is not True - else self.strategy.get_language() - } - - -class LinkedinOAuth(BaseLinkedinAuth, BaseOAuth1): - """Linkedin OAuth authentication backend""" - name = 'linkedin' - SCOPE_SEPARATOR = '+' - AUTHORIZATION_URL = 'https://www.linkedin.com/uas/oauth/authenticate' - REQUEST_TOKEN_URL = 'https://api.linkedin.com/uas/oauth/requestToken' - ACCESS_TOKEN_URL = 'https://api.linkedin.com/uas/oauth/accessToken' - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json( - self.user_details_url(), - params={'format': 'json'}, - auth=self.oauth_auth(access_token), - headers=self.user_data_headers() - ) - - def unauthorized_token(self): - """Makes first request to oauth. Returns an unauthorized Token.""" - scope = self.get_scope() or '' - if scope: - scope = '?scope=' + self.SCOPE_SEPARATOR.join(scope) - return self.request(self.REQUEST_TOKEN_URL + scope, - params=self.request_token_extra_arguments(), - auth=self.oauth_auth()).text - - -class LinkedinOAuth2(BaseLinkedinAuth, BaseOAuth2): - name = 'linkedin-oauth2' - SCOPE_SEPARATOR = ' ' - AUTHORIZATION_URL = 'https://www.linkedin.com/uas/oauth2/authorization' - ACCESS_TOKEN_URL = 'https://www.linkedin.com/uas/oauth2/accessToken' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def user_data(self, access_token, *args, **kwargs): - return self.get_json( - self.user_details_url(), - params={'oauth2_access_token': access_token, - 'format': 'json'}, - headers=self.user_data_headers() - ) - - def request_access_token(self, *args, **kwargs): - # LinkedIn expects a POST request with querystring parameters, despite - # the spec http://tools.ietf.org/html/rfc6749#section-4.1.3 - kwargs['params'] = kwargs.pop('data') - return super(LinkedinOAuth2, self).request_access_token( - *args, **kwargs - ) +from social_core.backends.linkedin import BaseLinkedinAuth, LinkedinOAuth, \ + LinkedinOAuth2 diff --git a/social/backends/live.py b/social/backends/live.py index a7dda92e8..912d55f25 100644 --- a/social/backends/live.py +++ b/social/backends/live.py @@ -1,44 +1 @@ -""" -Live OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/live.html -""" -from social.backends.oauth import BaseOAuth2 - - -class LiveOAuth2(BaseOAuth2): - name = 'live' - AUTHORIZATION_URL = 'https://login.live.com/oauth20_authorize.srf' - ACCESS_TOKEN_URL = 'https://login.live.com/oauth20_token.srf' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - DEFAULT_SCOPE = ['wl.basic', 'wl.emails'] - EXTRA_DATA = [ - ('id', 'id'), - ('access_token', 'access_token'), - ('authentication_token', 'authentication_token'), - ('refresh_token', 'refresh_token'), - ('expires_in', 'expires'), - ('email', 'email'), - ('first_name', 'first_name'), - ('last_name', 'last_name'), - ('token_type', 'token_type'), - ] - REDIRECT_STATE = False - - def get_user_details(self, response): - """Return user details from Live Connect account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('first_name'), - last_name=response.get('last_name') - ) - return {'username': response.get('name'), - 'email': response.get('emails', {}).get('account', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://apis.live.net/v5.0/me', params={ - 'access_token': access_token - }) +from social_core.backends.live import LiveOAuth2 diff --git a/social/backends/livejournal.py b/social/backends/livejournal.py index c2e92d117..f53263544 100644 --- a/social/backends/livejournal.py +++ b/social/backends/livejournal.py @@ -1,26 +1 @@ -""" -LiveJournal OpenId backend, docs at: - http://psa.matiasaguirre.net/docs/backends/livejournal.html -""" -from social.p3 import urlsplit -from social.backends.open_id import OpenIdAuth -from social.exceptions import AuthMissingParameter - - -class LiveJournalOpenId(OpenIdAuth): - """LiveJournal OpenID authentication backend""" - name = 'livejournal' - - def get_user_details(self, response): - """Generate username from identity url""" - values = super(LiveJournalOpenId, self).get_user_details(response) - values['username'] = values.get('username') or \ - urlsplit(response.identity_url)\ - .netloc.split('.', 1)[0] - return values - - def openid_url(self): - """Returns LiveJournal authentication URL""" - if not self.data.get('openid_lj_user'): - raise AuthMissingParameter(self, 'openid_lj_user') - return 'http://{0}.livejournal.com'.format(self.data['openid_lj_user']) +from social_core.backends.livejournal import LiveJournalOpenId diff --git a/social/backends/loginradius.py b/social/backends/loginradius.py index e7193b4cc..a815e1a2b 100644 --- a/social/backends/loginradius.py +++ b/social/backends/loginradius.py @@ -1,69 +1 @@ -""" -LoginRadius BaseOAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/loginradius.html -""" -from social.backends.oauth import BaseOAuth2 - - -class LoginRadiusAuth(BaseOAuth2): - """LoginRadius BaseOAuth2 authentication backend.""" - name = 'loginradius' - ID_KEY = 'ID' - ACCESS_TOKEN_URL = 'https://api.loginradius.com/api/v2/access_token' - PROFILE_URL = 'https://api.loginradius.com/api/v2/userprofile' - ACCESS_TOKEN_METHOD = 'GET' - REDIRECT_STATE = False - STATE_PARAMETER = False - - def uses_redirect(self): - """Return False because we return HTML instead.""" - return False - - def auth_html(self): - key, secret = self.get_key_and_secret() - tpl = self.setting('TEMPLATE', 'loginradius.html') - return self.strategy.render_html(tpl=tpl, context={ - 'backend': self, - 'LOGINRADIUS_KEY': key, - 'LOGINRADIUS_REDIRECT_URL': self.get_redirect_uri() - }) - - def request_access_token(self, *args, **kwargs): - return self.get_json(params={ - 'token': self.data.get('token'), - 'secret': self.setting('SECRET') - }, *args, **kwargs) - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service. Implement in subclass.""" - return self.get_json( - self.PROFILE_URL, - params={'access_token': access_token}, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - - def get_user_details(self, response): - """Must return user details in a know internal struct: - {'username': , - 'email': , - 'fullname': , - 'first_name': , - 'last_name': } - """ - profile = { - 'username': response['NickName'] or '', - 'email': response['Email'][0]['Value'] or '', - 'fullname': response['FullName'] or '', - 'first_name': response['FirstName'] or '', - 'last_name': response['LastName'] or '' - } - return profile - - def get_user_id(self, details, response): - """Return a unique ID for the current user, by default from server - response. Since LoginRadius handles multiple providers, we need to - distinguish them to prevent conflicts.""" - return '{0}-{1}'.format(response.get('Provider'), - response.get(self.ID_KEY)) +from social_core.backends.loginradius import LoginRadiusAuth diff --git a/social/backends/mailru.py b/social/backends/mailru.py index 7f9e17d4c..2d58795db 100644 --- a/social/backends/mailru.py +++ b/social/backends/mailru.py @@ -1,45 +1 @@ -""" -Mail.ru OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/mailru.html -""" -from hashlib import md5 - -from social.p3 import unquote -from social.backends.oauth import BaseOAuth2 - - -class MailruOAuth2(BaseOAuth2): - """Mail.ru authentication backend""" - name = 'mailru-oauth2' - ID_KEY = 'uid' - AUTHORIZATION_URL = 'https://connect.mail.ru/oauth/authorize' - ACCESS_TOKEN_URL = 'https://connect.mail.ru/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [('refresh_token', 'refresh_token'), - ('expires_in', 'expires')] - - def get_user_details(self, response): - """Return user details from Mail.ru request""" - fullname, first_name, last_name = self.get_user_names( - first_name=unquote(response['first_name']), - last_name=unquote(response['last_name']) - ) - return {'username': unquote(response['nick']), - 'email': unquote(response['email']), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data from Mail.ru REST API""" - key, secret = self.get_key_and_secret() - data = {'method': 'users.getInfo', - 'session_key': access_token, - 'app_id': key, - 'secure': '1'} - param_list = sorted(list(item + '=' + data[item] for item in data)) - data['sig'] = md5( - (''.join(param_list) + secret).encode('utf-8') - ).hexdigest() - return self.get_json('http://www.appsmail.ru/platform/api', - params=data)[0] +from social_core.backends.mailru import MailruOAuth2 diff --git a/social/backends/mapmyfitness.py b/social/backends/mapmyfitness.py index 6c6e6a94c..cdae4429a 100644 --- a/social/backends/mapmyfitness.py +++ b/social/backends/mapmyfitness.py @@ -1,49 +1 @@ -""" -MapMyFitness OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/mapmyfitness.html -""" -from social.backends.oauth import BaseOAuth2 - - -class MapMyFitnessOAuth2(BaseOAuth2): - """MapMyFitness OAuth authentication backend""" - name = 'mapmyfitness' - AUTHORIZATION_URL = 'https://www.mapmyfitness.com/v7.0/oauth2/authorize' - ACCESS_TOKEN_URL = \ - 'https://oauth2-api.mapmyapi.com/v7.0/oauth2/access_token' - REQUEST_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('refresh_token', 'refresh_token'), - ] - - def auth_headers(self): - key = self.get_key_and_secret()[0] - return { - 'Api-Key': key - } - - def get_user_id(self, details, response): - return response['id'] - - def get_user_details(self, response): - first = response.get('first_name', '') - last = response.get('last_name', '') - full = (first + last).strip() - return { - 'username': response['username'], - 'email': response['email'], - 'fullname': full, - 'first_name': first, - 'last_name': last, - } - - def user_data(self, access_token, *args, **kwargs): - key = self.get_key_and_secret()[0] - url = 'https://oauth2-api.mapmyapi.com/v7.0/user/self/' - headers = { - 'Authorization': 'Bearer {0}'.format(access_token), - 'Api-Key': key - } - return self.get_json(url, headers=headers) +from social_core.backends.mapmyfitness import MapMyFitnessOAuth2 diff --git a/social/backends/meetup.py b/social/backends/meetup.py index 297869714..d3e073563 100644 --- a/social/backends/meetup.py +++ b/social/backends/meetup.py @@ -1,34 +1 @@ -""" -Meetup OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/meetup.html -""" -from social.backends.oauth import BaseOAuth2 - - -class MeetupOAuth2(BaseOAuth2): - """Meetup OAuth2 authentication backend""" - name = 'meetup' - AUTHORIZATION_URL = 'https://secure.meetup.com/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://secure.meetup.com/oauth2/access' - ACCESS_TOKEN_METHOD = 'POST' - DEFAULT_SCOPE = ['basic'] - SCOPE_SEPARATOR = ',' - REDIRECT_STATE = False - STATE_PARAMETER = 'state' - - def get_user_details(self, response): - """Return user details from Meetup account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - - return {'username': response.get('username'), - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.meetup.com/2/member/self', - params={'access_token': access_token}) +from social_core.backends.meetup import MeetupOAuth2 diff --git a/social/backends/mendeley.py b/social/backends/mendeley.py index fde57a575..b31bcdfed 100644 --- a/social/backends/mendeley.py +++ b/social/backends/mendeley.py @@ -1,67 +1,2 @@ -""" -Mendeley OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/mendeley.html -""" -from social.backends.oauth import BaseOAuth1, BaseOAuth2 - - -class MendeleyMixin(object): - SCOPE_SEPARATOR = '+' - EXTRA_DATA = [('profile_id', 'profile_id'), - ('name', 'name'), - ('bio', 'bio')] - - def get_user_id(self, details, response): - return response['id'] - - def get_user_details(self, response): - """Return user details from Mendeley account""" - profile_id = response['id'] - name = response['display_name'] - bio = response['link'] - return {'profile_id': profile_id, - 'name': name, - 'bio': bio} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - values = self.get_user_data(access_token) - values.update(values) - return values - - def get_user_data(self, access_token): - raise NotImplementedError('Implement in subclass') - - -class MendeleyOAuth(MendeleyMixin, BaseOAuth1): - name = 'mendeley' - AUTHORIZATION_URL = 'http://api.mendeley.com/oauth/authorize/' - REQUEST_TOKEN_URL = 'http://api.mendeley.com/oauth/request_token/' - ACCESS_TOKEN_URL = 'http://api.mendeley.com/oauth/access_token/' - - def get_user_data(self, access_token): - return self.get_json( - 'http://api.mendeley.com/oapi/profiles/info/me/', - auth=self.oauth_auth(access_token) - ) - - -class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): - name = 'mendeley-oauth2' - AUTHORIZATION_URL = 'https://api-oauth2.mendeley.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api-oauth2.mendeley.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - DEFAULT_SCOPE = ['all'] - REDIRECT_STATE = False - EXTRA_DATA = MendeleyMixin.EXTRA_DATA + [ - ('refresh_token', 'refresh_token'), - ('expires_in', 'expires_in'), - ('token_type', 'token_type'), - ] - - def get_user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.mendeley.com/profiles/me/', - headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) +from social_core.backends.mendeley import MendeleyMixin, MendeleyOAuth, \ + MendeleyOAuth2 diff --git a/social/backends/mineid.py b/social/backends/mineid.py index 69dae18f3..81bdb1ede 100644 --- a/social/backends/mineid.py +++ b/social/backends/mineid.py @@ -1,38 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class MineIDOAuth2(BaseOAuth2): - """MineID OAuth2 authentication backend""" - name = 'mineid' - _AUTHORIZATION_URL = '%(scheme)s://%(host)s/oauth/authorize' - _ACCESS_TOKEN_URL = '%(scheme)s://%(host)s/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ] - - def get_user_details(self, response): - """Return user details""" - return {'email': response.get('email'), - 'username': response.get('email')} - - def user_data(self, access_token, *args, **kwargs): - return self._user_data(access_token) - - def _user_data(self, access_token, path=None): - url = '%(scheme)s://%(host)s/api/user' % self.get_mineid_url_params() - return self.get_json(url, params={'access_token': access_token}) - - @property - def AUTHORIZATION_URL(self): - return self._AUTHORIZATION_URL % self.get_mineid_url_params() - - @property - def ACCESS_TOKEN_URL(self): - return self._ACCESS_TOKEN_URL % self.get_mineid_url_params() - - def get_mineid_url_params(self): - return { - 'host': self.setting('HOST', 'www.mineid.org'), - 'scheme': self.setting('SCHEME', 'https'), - } +from social_core.backends.mineid import MineIDOAuth2 diff --git a/social/backends/mixcloud.py b/social/backends/mixcloud.py index 61e1605bf..abb2ff918 100644 --- a/social/backends/mixcloud.py +++ b/social/backends/mixcloud.py @@ -1,26 +1 @@ -""" -Mixcloud OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/mixcloud.html -""" -from social.backends.oauth import BaseOAuth2 - - -class MixcloudOAuth2(BaseOAuth2): - name = 'mixcloud' - ID_KEY = 'username' - AUTHORIZATION_URL = 'https://www.mixcloud.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://www.mixcloud.com/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names(response['name']) - return {'username': response['username'], - 'email': None, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://api.mixcloud.com/me/', - params={'access_token': access_token, - 'alt': 'json'}) +from social_core.backends.mixcloud import MixcloudOAuth2 diff --git a/social/backends/moves.py b/social/backends/moves.py index 5464f7bed..2fcac7a66 100644 --- a/social/backends/moves.py +++ b/social/backends/moves.py @@ -1,30 +1 @@ -""" -Moves OAuth2 backend, docs at: - https://dev.moves-app.com/docs/authentication - -Written by Avi Alkalay -Certified to work with Django 1.6 -""" -from social.backends.oauth import BaseOAuth2 - - -class MovesOAuth2(BaseOAuth2): - """Moves OAuth authentication backend""" - name = 'moves' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://api.moves-app.com/oauth/v1/authorize' - ACCESS_TOKEN_URL = 'https://api.moves-app.com/oauth/v1/access_token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('refresh_token', 'refresh_token', True), - ('expires_in', 'expires'), - ] - - def get_user_details(self, response): - """Return user details Moves account""" - return {'username': response.get('user_id')} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.moves-app.com/api/1.1/user/profile', - params={'access_token': access_token}) +from social_core.backends.moves import MovesOAuth2 diff --git a/social/backends/nationbuilder.py b/social/backends/nationbuilder.py index ae16c7fa8..659677bb7 100644 --- a/social/backends/nationbuilder.py +++ b/social/backends/nationbuilder.py @@ -1,48 +1 @@ -""" -NationBuilder OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/nationbuilder.html -""" -from social.backends.oauth import BaseOAuth2 - - -class NationBuilderOAuth2(BaseOAuth2): - """NationBuilder OAuth2 authentication backend""" - name = 'nationbuilder' - AUTHORIZATION_URL = 'https://{slug}.nationbuilder.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://{slug}.nationbuilder.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires') - ] - - def authorization_url(self): - return self.AUTHORIZATION_URL.format(slug=self.slug) - - def access_token_url(self): - return self.ACCESS_TOKEN_URL.format(slug=self.slug) - - @property - def slug(self): - return self.setting('SLUG') - - def get_user_details(self, response): - """Return user details from Github account""" - email = response.get('email') or '' - username = email.split('@')[0] if email else '' - return {'username': username, - 'email': email, - 'fullname': response.get('full_name') or '', - 'first_name': response.get('first_name') or '', - 'last_name': response.get('last_name') or ''} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - url = 'https://{slug}.nationbuilder.com/api/v1/people/me'.format( - slug=self.slug - ) - return self.get_json(url, params={ - 'access_token': access_token - })['person'] +from social_core.backends.nationbuilder import NationBuilderOAuth2 diff --git a/social/backends/naver.py b/social/backends/naver.py index 89acfdbb6..eb9869a73 100644 --- a/social/backends/naver.py +++ b/social/backends/naver.py @@ -1,59 +1 @@ -from xml.dom import minidom - -from social.backends.oauth import BaseOAuth2 - -class NaverOAuth2(BaseOAuth2): - """Naver OAuth authentication backend""" - name = 'naver' - AUTHORIZATION_URL = 'https://nid.naver.com/oauth2.0/authorize' - ACCESS_TOKEN_URL = 'https://nid.naver.com/oauth2.0/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('id', 'id'), - ] - - def get_user_id(self, details, response): - return response.get('id') - - def get_user_details(self, response): - """Return user details from Naver account""" - return { - 'username': response.get('username'), - 'email': response.get('email'), - 'fullname': response.get('username'), - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - response = self.request( - 'https://openapi.naver.com/v1/nid/getUserProfile.xml', - headers={ - 'Authorization': 'Bearer {0}'.format(access_token), - 'Content_Type': 'text/xml' - } - ) - - dom = minidom.parseString(response.text.encode('utf-8').strip()) - - return { - 'id': self._dom_value(dom, 'id'), - 'email': self._dom_value(dom, 'email'), - 'username': self._dom_value(dom, 'name'), - 'nickname': self._dom_value(dom, 'nickname'), - 'gender': self._dom_value(dom, 'gender'), - 'age': self._dom_value(dom, 'age'), - 'birthday': self._dom_value(dom, 'birthday'), - 'profile_image': self._dom_value(dom, 'profile_image') - } - - def auth_headers(self): - client_id, client_secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', - 'code': self.data.get('code'), - 'client_id': client_id, - 'client_secret': client_secret, - } - - def _dom_value(self, dom, key): - return dom.getElementsByTagName(key)[0].childNodes[0].data +from social_core.backends.naver import NaverOAuth2 diff --git a/social/backends/ngpvan.py b/social/backends/ngpvan.py index 0700eeb93..8038371aa 100644 --- a/social/backends/ngpvan.py +++ b/social/backends/ngpvan.py @@ -1,66 +1 @@ -""" -NGP VAN's `ActionID` Provider - -http://developers.ngpvan.com/action-id -""" -from openid.extensions import ax - -from social.backends.open_id import OpenIdAuth - - -class ActionIDOpenID(OpenIdAuth): - """ - NGP VAN's ActionID OpenID 1.1 authentication backend - """ - name = 'actionid-openid' - URL = 'https://accounts.ngpvan.com/Home/Xrds' - USERNAME_KEY = 'email' - - def get_ax_attributes(self): - """ - Return the AX attributes that ActionID responds with, as well as the - user data result that it must map to. - """ - return [ - ('http://openid.net/schema/contact/internet/email', 'email'), - ('http://openid.net/schema/contact/phone/business', 'phone'), - ('http://openid.net/schema/namePerson/first', 'first_name'), - ('http://openid.net/schema/namePerson/last', 'last_name'), - ('http://openid.net/schema/namePerson', 'fullname'), - ] - - def setup_request(self, params=None): - """ - Setup the OpenID request - - Because ActionID does not advertise the availiability of AX attributes - nor use standard attribute aliases, we need to setup the attributes - manually instead of rely on the parent OpenIdAuth.setup_request() - """ - request = self.openid_request(params) - - fetch_request = ax.FetchRequest() - fetch_request.add(ax.AttrInfo( - 'http://openid.net/schema/contact/internet/email', - alias='ngpvanemail', - required=True - )) - - fetch_request.add(ax.AttrInfo( - 'http://openid.net/schema/contact/phone/business', - alias='ngpvanphone', - required=False - )) - fetch_request.add(ax.AttrInfo( - 'http://openid.net/schema/namePerson/first', - alias='ngpvanfirstname', - required=False - )) - fetch_request.add(ax.AttrInfo( - 'http://openid.net/schema/namePerson/last', - alias='ngpvanlastname', - required=False - )) - request.addExtension(fetch_request) - - return request +from social_core.backends.ngpvan import ActionIDOpenID diff --git a/social/backends/nk.py b/social/backends/nk.py index 1446ad179..b5dfac2ed 100644 --- a/social/backends/nk.py +++ b/social/backends/nk.py @@ -1,74 +1 @@ -from urllib import urlencode - -import six - -from requests_oauthlib import OAuth1 - -from social.backends.oauth import BaseOAuth2 - - -class NKOAuth2(BaseOAuth2): - """NK OAuth authentication backend""" - name = 'nk' - AUTHORIZATION_URL = 'https://nk.pl/oauth2/login' - ACCESS_TOKEN_URL = 'https://nk.pl/oauth2/token' - SCOPE_SEPARATOR = ',' - ACCESS_TOKEN_METHOD = 'POST' - SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER' - EXTRA_DATA = [ - ('id', 'id'), - ] - - def get_user_details(self, response): - """Return user details from NK account""" - entry = response['entry'] - return { - 'username': entry.get('displayName'), - 'email': entry['emails'][0]['value'], - 'first_name': entry.get('displayName').split(' ')[0], - 'id': entry.get('id') - } - - def auth_complete_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'client_id': client_id, - 'client_secret': client_secret, - 'redirect_uri': self.get_redirect_uri(state), - 'scope': self.get_scope_argument() - } - - def get_user_id(self, details, response): - """Return a unique ID for the current user, by default from server - response.""" - return details.get(self.ID_KEY) - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - url = 'http://opensocial.nk-net.pl/v09/social/rest/people/@me?' + urlencode({ - 'nk_token': access_token, - 'fields': 'name,surname,avatar,localization,age,gender,emails,birthdate' - }) - return self.get_json( - url, - auth=self.oauth_auth(access_token) - ) - - def oauth_auth(self, token=None, oauth_verifier=None, - signature_type=SIGNATURE_TYPE_AUTH_HEADER): - key, secret = self.get_key_and_secret() - oauth_verifier = oauth_verifier or self.data.get('oauth_verifier') - token = token or {} - # decoding='utf-8' produces errors with python-requests on Python3 - # since the final URL will be of type bytes - decoding = None if six.PY3 else 'utf-8' - state = self.get_or_create_state() - return OAuth1(key, secret, - resource_owner_key=None, - resource_owner_secret=None, - callback_uri=self.get_redirect_uri(state), - verifier=oauth_verifier, - signature_type=signature_type, - decoding=decoding) +from social_core.backends.nk import NKOAuth2 diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 3182e52b0..89c7a1ac8 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -1,434 +1 @@ -import six - -from requests_oauthlib import OAuth1 -from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER - -from social.p3 import urlencode, unquote -from social.utils import url_add_parameters, parse_qs, handle_http_errors -from social.exceptions import AuthFailed, AuthCanceled, AuthUnknownError, \ - AuthMissingParameter, AuthStateMissing, \ - AuthStateForbidden, AuthTokenError -from social.backends.base import BaseAuth - - -class OAuthAuth(BaseAuth): - """OAuth authentication backend base class. - - Also settings will be inspected to get more values names that should be - stored on extra_data field. Setting name is created from current backend - name (all uppercase) plus _EXTRA_DATA. - - access_token is always stored. - - URLs settings: - AUTHORIZATION_URL Authorization service url - ACCESS_TOKEN_URL Access token URL - """ - AUTHORIZATION_URL = '' - ACCESS_TOKEN_URL = '' - ACCESS_TOKEN_METHOD = 'GET' - REVOKE_TOKEN_URL = None - REVOKE_TOKEN_METHOD = 'POST' - ID_KEY = 'id' - SCOPE_PARAMETER_NAME = 'scope' - DEFAULT_SCOPE = None - SCOPE_SEPARATOR = ' ' - REDIRECT_STATE = False - STATE_PARAMETER = False - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return access_token and extra defined names to store in - extra_data field""" - data = super(OAuthAuth, self).extra_data(user, uid, response, details, - *args, **kwargs) - data['access_token'] = response.get('access_token', '') or \ - kwargs.get('access_token') - return data - - def state_token(self): - """Generate csrf token to include as state parameter.""" - return self.strategy.random_string(32) - - def get_or_create_state(self): - if self.STATE_PARAMETER or self.REDIRECT_STATE: - # Store state in session for further request validation. The state - # value is passed as state parameter (as specified in OAuth2 spec), - # but also added to redirect, that way we can still verify the - # request if the provider doesn't implement the state parameter. - # Reuse token if any. - name = self.name + '_state' - state = self.strategy.session_get(name) - if state is None: - state = self.state_token() - self.strategy.session_set(name, state) - else: - state = None - return state - - def get_session_state(self): - return self.strategy.session_get(self.name + '_state') - - def get_request_state(self): - request_state = self.data.get('state') or \ - self.data.get('redirect_state') - if request_state and isinstance(request_state, list): - request_state = request_state[0] - return request_state - - def validate_state(self): - """Validate state value. Raises exception on error, returns state - value if valid.""" - if not self.STATE_PARAMETER and not self.REDIRECT_STATE: - return None - state = self.get_session_state() - request_state = self.get_request_state() - if not request_state: - raise AuthMissingParameter(self, 'state') - elif not state: - raise AuthStateMissing(self, 'state') - elif not request_state == state: - raise AuthStateForbidden(self) - else: - return state - - def get_redirect_uri(self, state=None): - """Build redirect with redirect_state parameter.""" - uri = self.redirect_uri - if self.REDIRECT_STATE and state: - uri = url_add_parameters(uri, {'redirect_state': state}) - return uri - - def get_scope(self): - """Return list with needed access scope""" - scope = self.setting('SCOPE', []) - if not self.setting('IGNORE_DEFAULT_SCOPE', False): - scope = scope + (self.DEFAULT_SCOPE or []) - return scope - - def get_scope_argument(self): - param = {} - scope = self.get_scope() - if scope: - param[self.SCOPE_PARAMETER_NAME] = self.SCOPE_SEPARATOR.join(scope) - return param - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service. Implement in subclass""" - return {} - - def authorization_url(self): - return self.AUTHORIZATION_URL - - def access_token_url(self): - return self.ACCESS_TOKEN_URL - - def revoke_token_url(self, token, uid): - return self.REVOKE_TOKEN_URL - - def revoke_token_params(self, token, uid): - return {} - - def revoke_token_headers(self, token, uid): - return {} - - def process_revoke_token_response(self, response): - return response.status_code == 200 - - def revoke_token(self, token, uid): - if self.REVOKE_TOKEN_URL: - url = self.revoke_token_url(token, uid) - params = self.revoke_token_params(token, uid) - headers = self.revoke_token_headers(token, uid) - data = urlencode(params) if self.REVOKE_TOKEN_METHOD != 'GET' \ - else None - response = self.request(url, params=params, headers=headers, - data=data, method=self.REVOKE_TOKEN_METHOD) - return self.process_revoke_token_response(response) - - -class BaseOAuth1(OAuthAuth): - """Consumer based mechanism OAuth authentication, fill the needed - parameters to communicate properly with authentication service. - - URLs settings: - REQUEST_TOKEN_URL Request token URL - - """ - REQUEST_TOKEN_URL = '' - REQUEST_TOKEN_METHOD = 'GET' - OAUTH_TOKEN_PARAMETER_NAME = 'oauth_token' - REDIRECT_URI_PARAMETER_NAME = 'redirect_uri' - UNATHORIZED_TOKEN_SUFIX = 'unauthorized_token_name' - - def auth_url(self): - """Return redirect url""" - token = self.set_unauthorized_token() - return self.oauth_authorization_request(token) - - def process_error(self, data): - if 'oauth_problem' in data: - if data['oauth_problem'] == 'user_refused': - raise AuthCanceled(self, 'User refused the access') - raise AuthUnknownError(self, 'Error was ' + data['oauth_problem']) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Return user, might be logged in""" - # Multiple unauthorized tokens are supported (see #521) - self.process_error(self.data) - self.validate_state() - token = self.get_unauthorized_token() - access_token = self.access_token(token) - return self.do_auth(access_token, *args, **kwargs) - - @handle_http_errors - def do_auth(self, access_token, *args, **kwargs): - """Finish the auth process once the access_token was retrieved""" - if not isinstance(access_token, dict): - access_token = parse_qs(access_token) - data = self.user_data(access_token) - if data is not None and 'access_token' not in data: - data['access_token'] = access_token - kwargs.update({'response': data, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def get_unauthorized_token(self): - name = self.name + self.UNATHORIZED_TOKEN_SUFIX - unauthed_tokens = self.strategy.session_get(name, []) - if not unauthed_tokens: - raise AuthTokenError(self, 'Missing unauthorized token') - - data_token = self.data.get(self.OAUTH_TOKEN_PARAMETER_NAME) - - if data_token is None: - raise AuthTokenError(self, 'Missing unauthorized token') - - token = None - for utoken in unauthed_tokens: - orig_utoken = utoken - if not isinstance(utoken, dict): - utoken = parse_qs(utoken) - if utoken.get(self.OAUTH_TOKEN_PARAMETER_NAME) == data_token: - self.strategy.session_set(name, list(set(unauthed_tokens) - - set([orig_utoken]))) - token = utoken - break - else: - raise AuthTokenError(self, 'Incorrect tokens') - return token - - def set_unauthorized_token(self): - token = self.unauthorized_token() - name = self.name + self.UNATHORIZED_TOKEN_SUFIX - tokens = self.strategy.session_get(name, []) + [token] - self.strategy.session_set(name, tokens) - return token - - def request_token_extra_arguments(self): - """Return extra arguments needed on request-token process""" - return self.setting('REQUEST_TOKEN_EXTRA_ARGUMENTS', {}) - - def unauthorized_token(self): - """Return request for unauthorized token (first stage)""" - params = self.request_token_extra_arguments() - params.update(self.get_scope_argument()) - key, secret = self.get_key_and_secret() - # decoding='utf-8' produces errors with python-requests on Python3 - # since the final URL will be of type bytes - decoding = None if six.PY3 else 'utf-8' - state = self.get_or_create_state() - response = self.request( - self.REQUEST_TOKEN_URL, - params=params, - auth=OAuth1(key, secret, callback_uri=self.get_redirect_uri(state), - decoding=decoding), - method=self.REQUEST_TOKEN_METHOD - ) - content = response.content - if response.encoding or response.apparent_encoding: - content = content.decode(response.encoding or - response.apparent_encoding) - else: - content = response.content.decode() - return content - - def oauth_authorization_request(self, token): - """Generate OAuth request to authorize token.""" - if not isinstance(token, dict): - token = parse_qs(token) - params = self.auth_extra_arguments() or {} - params.update(self.get_scope_argument()) - params[self.OAUTH_TOKEN_PARAMETER_NAME] = token.get( - self.OAUTH_TOKEN_PARAMETER_NAME - ) - state = self.get_or_create_state() - params[self.REDIRECT_URI_PARAMETER_NAME] = self.get_redirect_uri(state) - return '{0}?{1}'.format(self.authorization_url(), urlencode(params)) - - def oauth_auth(self, token=None, oauth_verifier=None, - signature_type=SIGNATURE_TYPE_AUTH_HEADER): - key, secret = self.get_key_and_secret() - oauth_verifier = oauth_verifier or self.data.get('oauth_verifier') - if token: - resource_owner_key = token.get('oauth_token') - resource_owner_secret = token.get('oauth_token_secret') - if not resource_owner_key: - raise AuthTokenError(self, 'Missing oauth_token') - if not resource_owner_secret: - raise AuthTokenError(self, 'Missing oauth_token_secret') - else: - resource_owner_key = None - resource_owner_secret = None - # decoding='utf-8' produces errors with python-requests on Python3 - # since the final URL will be of type bytes - decoding = None if six.PY3 else 'utf-8' - state = self.get_or_create_state() - return OAuth1(key, secret, - resource_owner_key=resource_owner_key, - resource_owner_secret=resource_owner_secret, - callback_uri=self.get_redirect_uri(state), - verifier=oauth_verifier, - signature_type=signature_type, - decoding=decoding) - - def oauth_request(self, token, url, params=None, method='GET'): - """Generate OAuth request, setups callback url""" - return self.request(url, method=method, params=params, - auth=self.oauth_auth(token)) - - def access_token(self, token): - """Return request for access token value""" - return self.get_querystring(self.access_token_url(), - auth=self.oauth_auth(token), - method=self.ACCESS_TOKEN_METHOD) - - -class BaseOAuth2(OAuthAuth): - """Base class for OAuth2 providers. - - OAuth2 draft details at: - http://tools.ietf.org/html/draft-ietf-oauth-v2-10 - """ - REFRESH_TOKEN_URL = None - REFRESH_TOKEN_METHOD = 'POST' - RESPONSE_TYPE = 'code' - REDIRECT_STATE = True - STATE_PARAMETER = True - - def auth_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - params = { - 'client_id': client_id, - 'redirect_uri': self.get_redirect_uri(state) - } - if self.STATE_PARAMETER and state: - params['state'] = state - if self.RESPONSE_TYPE: - params['response_type'] = self.RESPONSE_TYPE - return params - - def auth_url(self): - """Return redirect url""" - state = self.get_or_create_state() - params = self.auth_params(state) - params.update(self.get_scope_argument()) - params.update(self.auth_extra_arguments()) - params = urlencode(params) - if not self.REDIRECT_STATE: - # redirect_uri matching is strictly enforced, so match the - # providers value exactly. - params = unquote(params) - return '{0}?{1}'.format(self.authorization_url(), params) - - def auth_complete_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'client_id': client_id, - 'client_secret': client_secret, - 'redirect_uri': self.get_redirect_uri(state) - } - - def auth_complete_credentials(self): - return None - - def auth_headers(self): - return {'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json'} - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return access_token, token_type, and extra defined names to store in - extra_data field""" - data = super(BaseOAuth2, self).extra_data(user, uid, response, - details=details, - *args, **kwargs) - data['token_type'] = response.get('token_type') or \ - kwargs.get('token_type') - return data - - def request_access_token(self, *args, **kwargs): - return self.get_json(*args, **kwargs) - - def process_error(self, data): - if data.get('error'): - if data['error'] == 'denied' or data['error'] == 'access_denied': - raise AuthCanceled(self, data.get('error_description', '')) - raise AuthFailed(self, data.get('error_description') or - data['error']) - elif 'denied' in data: - raise AuthCanceled(self, data['denied']) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - state = self.validate_state() - self.process_error(self.data) - - response = self.request_access_token( - self.access_token_url(), - data=self.auth_complete_params(state), - headers=self.auth_headers(), - auth=self.auth_complete_credentials(), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) - - @handle_http_errors - def do_auth(self, access_token, *args, **kwargs): - """Finish the auth process once the access_token was retrieved""" - data = self.user_data(access_token, *args, **kwargs) - response = kwargs.get('response') or {} - response.update(data or {}) - if 'access_token' not in response: - response['access_token'] = access_token - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def refresh_token_params(self, token, *args, **kwargs): - client_id, client_secret = self.get_key_and_secret() - return { - 'refresh_token': token, - 'grant_type': 'refresh_token', - 'client_id': client_id, - 'client_secret': client_secret - } - - def process_refresh_token_response(self, response, *args, **kwargs): - return response.json() - - def refresh_token(self, token, *args, **kwargs): - params = self.refresh_token_params(token, *args, **kwargs) - url = self.refresh_token_url() - method = self.REFRESH_TOKEN_METHOD - key = 'params' if method == 'GET' else 'data' - request_args = {'headers': self.auth_headers(), - 'method': method, - key: params} - request = self.request(url, **request_args) - return self.process_refresh_token_response(request, *args, **kwargs) - - def refresh_token_url(self): - return self.REFRESH_TOKEN_URL or self.access_token_url() +from social_core.backends.oauth import OAuthAuth, BaseOAuth1, BaseOAuth2 diff --git a/social/backends/odnoklassniki.py b/social/backends/odnoklassniki.py index 4981b0339..d495f9adf 100644 --- a/social/backends/odnoklassniki.py +++ b/social/backends/odnoklassniki.py @@ -1,171 +1,3 @@ -""" -Odnoklassniki OAuth2 and Iframe Application backends, docs at: - http://psa.matiasaguirre.net/docs/backends/odnoklassnikiru.html -""" -from hashlib import md5 - -from social.p3 import unquote -from social.backends.base import BaseAuth -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthFailed - - -class OdnoklassnikiOAuth2(BaseOAuth2): - """Odnoklassniki authentication backend""" - name = 'odnoklassniki-oauth2' - ID_KEY = 'uid' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ';' - AUTHORIZATION_URL = 'https://connect.ok.ru/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api.ok.ru/oauth/token.do' - EXTRA_DATA = [('refresh_token', 'refresh_token'), - ('expires_in', 'expires')] - - def get_user_details(self, response): - """Return user details from Odnoklassniki request""" - fullname, first_name, last_name = self.get_user_names( - fullname=unquote(response['name']), - first_name=unquote(response['first_name']), - last_name=unquote(response['last_name']) - ) - return { - 'username': response['uid'], - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Return user data from Odnoklassniki REST API""" - data = {'access_token': access_token, 'method': 'users.getCurrentUser'} - key, secret = self.get_key_and_secret() - public_key = self.setting('PUBLIC_NAME') - return odnoklassniki_api(self, data, 'https://api.ok.ru/', - public_key, secret, 'oauth') - - -class OdnoklassnikiApp(BaseAuth): - """Odnoklassniki iframe app authentication backend""" - name = 'odnoklassniki-app' - ID_KEY = 'uid' - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - return dict([(key, value) for key, value in response.items() - if key in response['extra_data_list']]) - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - fullname=unquote(response['name']), - first_name=unquote(response['first_name']), - last_name=unquote(response['last_name']) - ) - return { - 'username': response['uid'], - 'email': '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def auth_complete(self, *args, **kwargs): - self.verify_auth_sig() - response = self.get_response() - fields = ('uid', 'first_name', 'last_name', 'name') + \ - self.setting('EXTRA_USER_DATA_LIST', ()) - data = { - 'method': 'users.getInfo', - 'uids': '{0}'.format(response['logged_user_id']), - 'fields': ','.join(fields), - } - client_key, client_secret = self.get_key_and_secret() - public_key = self.setting('PUBLIC_NAME') - details = odnoklassniki_api(self, data, response['api_server'], - public_key, client_secret, - 'iframe_nosession') - if len(details) == 1 and 'uid' in details[0]: - details = details[0] - auth_data_fields = self.setting('EXTRA_AUTH_DATA_LIST', - ('api_server', 'apiconnection', - 'session_key', 'authorized', - 'session_secret_key')) - - for field in auth_data_fields: - details[field] = response[field] - details['extra_data_list'] = fields + auth_data_fields - kwargs.update({'backend': self, 'response': details}) - else: - raise AuthFailed(self, 'Cannot get user details: API error') - return self.strategy.authenticate(*args, **kwargs) - - def get_auth_sig(self): - secret_key = self.setting('SECRET') - hash_source = '{0:s}{1:s}{2:s}'.format(self.data['logged_user_id'], - self.data['session_key'], - secret_key) - return md5(hash_source.encode('utf-8')).hexdigest() - - def get_response(self): - fields = ('logged_user_id', 'api_server', 'application_key', - 'session_key', 'session_secret_key', 'authorized', - 'apiconnection') - return dict((name, self.data[name]) for name in fields - if name in self.data) - - def verify_auth_sig(self): - correct_key = self.get_auth_sig() - key = self.data['auth_sig'].lower() - if correct_key != key: - raise AuthFailed(self, 'Wrong authorization key') - - -def odnoklassniki_oauth_sig(data, client_secret): - """ - Calculates signature of request data access_token value must be included - Algorithm is described at - https://apiok.ru/wiki/pages/viewpage.action?pageId=12878032, - search for "little bit different way" - """ - suffix = md5( - '{0:s}{1:s}'.format(data['access_token'], - client_secret).encode('utf-8') - ).hexdigest() - check_list = sorted(['{0:s}={1:s}'.format(key, value) - for key, value in data.items() - if key != 'access_token']) - return md5((''.join(check_list) + suffix).encode('utf-8')).hexdigest() - - -def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): - """ - Calculates signature as described at: - https://apiok.ru/wiki/display/ok/Authentication+and+Authorization - If API method requires session context, request is signed with session - secret key. Otherwise it is signed with application secret key - """ - param_list = sorted(['{0:s}={1:s}'.format(key, value) - for key, value in data.items()]) - return md5( - (''.join(param_list) + client_secret_or_session_secret).encode('utf-8') - ).hexdigest() - - -def odnoklassniki_api(backend, data, api_url, public_key, client_secret, - request_type='oauth'): - """Calls Odnoklassniki REST API method - https://apiok.ru/wiki/display/ok/Odnoklassniki+Rest+API""" - data.update({ - 'application_key': public_key, - 'format': 'JSON' - }) - if request_type == 'oauth': - data['sig'] = odnoklassniki_oauth_sig(data, client_secret) - elif request_type == 'iframe_session': - data['sig'] = odnoklassniki_iframe_sig(data, - data['session_secret_key']) - elif request_type == 'iframe_nosession': - data['sig'] = odnoklassniki_iframe_sig(data, client_secret) - else: - msg = 'Unknown request type {0}. How should it be signed?' - raise AuthFailed(backend, msg.format(request_type)) - return backend.get_json(api_url + 'fb.do', params=data) +from social_core.backends.odnoklassniki import OdnoklassnikiOAuth2, \ + OdnoklassnikiApp, odnoklassniki_oauth_sig, odnoklassniki_iframe_sig, \ + odnoklassniki_api diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 1b525eb22..71ab7caf6 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -1,379 +1,2 @@ -import datetime -from calendar import timegm - -from jwt import InvalidTokenError, decode as jwt_decode - -from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE -from openid.consumer.discover import DiscoveryFailure -from openid.extensions import sreg, ax, pape - -from social.utils import url_add_parameters -from social.backends.base import BaseAuth -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthMissingParameter, \ - AuthTokenError - - -# OpenID configuration -OLD_AX_ATTRS = [ - ('http://schema.openid.net/contact/email', 'old_email'), - ('http://schema.openid.net/namePerson', 'old_fullname'), - ('http://schema.openid.net/namePerson/friendly', 'old_nickname') -] -AX_SCHEMA_ATTRS = [ - # Request both the full name and first/last components since some - # providers offer one but not the other. - ('http://axschema.org/contact/email', 'email'), - ('http://axschema.org/namePerson', 'fullname'), - ('http://axschema.org/namePerson/first', 'first_name'), - ('http://axschema.org/namePerson/last', 'last_name'), - ('http://axschema.org/namePerson/friendly', 'nickname'), -] -SREG_ATTR = [ - ('email', 'email'), - ('fullname', 'fullname'), - ('nickname', 'nickname') -] -OPENID_ID_FIELD = 'openid_identifier' -SESSION_NAME = 'openid' - - -class OpenIdAuth(BaseAuth): - """Generic OpenID authentication backend""" - name = 'openid' - URL = None - USERNAME_KEY = 'username' - - def get_user_id(self, details, response): - """Return user unique id provided by service""" - return response.identity_url - - def get_ax_attributes(self): - attrs = self.setting('AX_SCHEMA_ATTRS', []) - if attrs and self.setting('IGNORE_DEFAULT_AX_ATTRS', True): - return attrs - return attrs + AX_SCHEMA_ATTRS + OLD_AX_ATTRS - - def get_sreg_attributes(self): - return self.setting('SREG_ATTR') or SREG_ATTR - - def values_from_response(self, response, sreg_names=None, ax_names=None): - """Return values from SimpleRegistration response or - AttributeExchange response if present. - - @sreg_names and @ax_names must be a list of name and aliases - for such name. The alias will be used as mapping key. - """ - values = {} - - # Use Simple Registration attributes if provided - if sreg_names: - resp = sreg.SRegResponse.fromSuccessResponse(response) - if resp: - values.update((alias, resp.get(name) or '') - for name, alias in sreg_names) - - # Use Attribute Exchange attributes if provided - if ax_names: - resp = ax.FetchResponse.fromSuccessResponse(response) - if resp: - for src, alias in ax_names: - name = alias.replace('old_', '') - values[name] = resp.getSingle(src, '') or values.get(name) - return values - - def get_user_details(self, response): - """Return user details from an OpenID request""" - values = {'username': '', 'email': '', 'fullname': '', - 'first_name': '', 'last_name': ''} - # update values using SimpleRegistration or AttributeExchange - # values - values.update(self.values_from_response( - response, self.get_sreg_attributes(), self.get_ax_attributes() - )) - - fullname = values.get('fullname') or '' - first_name = values.get('first_name') or '' - last_name = values.get('last_name') or '' - email = values.get('email') or '' - - if not fullname and first_name and last_name: - fullname = first_name + ' ' + last_name - elif fullname: - try: - first_name, last_name = fullname.rsplit(' ', 1) - except ValueError: - last_name = fullname - - username_key = self.setting('USERNAME_KEY') or self.USERNAME_KEY - values.update({'fullname': fullname, 'first_name': first_name, - 'last_name': last_name, - 'username': values.get(username_key) or - (first_name.title() + last_name.title()), - 'email': email}) - return values - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return defined extra data names to store in extra_data field. - Settings will be inspected to get more values names that should be - stored on extra_data field. Setting name is created from current - backend name (all uppercase) plus _SREG_EXTRA_DATA and - _AX_EXTRA_DATA because values can be returned by SimpleRegistration - or AttributeExchange schemas. - - Both list must be a value name and an alias mapping similar to - SREG_ATTR, OLD_AX_ATTRS or AX_SCHEMA_ATTRS - """ - sreg_names = self.setting('SREG_EXTRA_DATA') - ax_names = self.setting('AX_EXTRA_DATA') - values = self.values_from_response(response, sreg_names, ax_names) - from_details = super(OpenIdAuth, self).extra_data( - user, uid, {}, details, *args, **kwargs - ) - values.update(from_details) - return values - - def auth_url(self): - """Return auth URL returned by service""" - openid_request = self.setup_request(self.auth_extra_arguments()) - # Construct completion URL, including page we should redirect to - return_to = self.strategy.absolute_uri(self.redirect_uri) - return openid_request.redirectURL(self.trust_root(), return_to) - - def auth_html(self): - """Return auth HTML returned by service""" - openid_request = self.setup_request(self.auth_extra_arguments()) - return_to = self.strategy.absolute_uri(self.redirect_uri) - form_tag = {'id': 'openid_message'} - return openid_request.htmlMarkup(self.trust_root(), return_to, - form_tag_attrs=form_tag) - - def trust_root(self): - """Return trust-root option""" - return self.setting('OPENID_TRUST_ROOT') or \ - self.strategy.absolute_uri('/') - - def continue_pipeline(self, *args, **kwargs): - """Continue previous halted pipeline""" - response = self.consumer().complete(dict(self.data.items()), - self.strategy.absolute_uri( - self.redirect_uri - )) - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def auth_complete(self, *args, **kwargs): - """Complete auth process""" - response = self.consumer().complete(dict(self.data.items()), - self.strategy.absolute_uri( - self.redirect_uri - )) - self.process_error(response) - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def process_error(self, data): - if not data: - raise AuthException(self, 'OpenID relying party endpoint') - elif data.status == FAILURE: - raise AuthFailed(self, data.message) - elif data.status == CANCEL: - raise AuthCanceled(self) - elif data.status != SUCCESS: - raise AuthUnknownError(self, data.status) - - def setup_request(self, params=None): - """Setup request""" - request = self.openid_request(params) - # Request some user details. Use attribute exchange if provider - # advertises support. - if request.endpoint.supportsType(ax.AXMessage.ns_uri): - fetch_request = ax.FetchRequest() - # Mark all attributes as required, Google ignores optional ones - for attr, alias in self.get_ax_attributes(): - fetch_request.add(ax.AttrInfo(attr, alias=alias, - required=True)) - else: - fetch_request = sreg.SRegRequest( - optional=list(dict(self.get_sreg_attributes()).keys()) - ) - request.addExtension(fetch_request) - - # Add PAPE Extension for if configured - preferred_policies = self.setting( - 'OPENID_PAPE_PREFERRED_AUTH_POLICIES' - ) - preferred_level_types = self.setting( - 'OPENID_PAPE_PREFERRED_AUTH_LEVEL_TYPES' - ) - max_age = self.setting('OPENID_PAPE_MAX_AUTH_AGE') - if max_age is not None: - try: - max_age = int(max_age) - except (ValueError, TypeError): - max_age = None - - if max_age is not None or preferred_policies or preferred_level_types: - pape_request = pape.Request( - max_auth_age=max_age, - preferred_auth_policies=preferred_policies, - preferred_auth_level_types=preferred_level_types - ) - request.addExtension(pape_request) - return request - - def consumer(self): - """Create an OpenID Consumer object for the given Django request.""" - if not hasattr(self, '_consumer'): - self._consumer = self.create_consumer(self.strategy.openid_store()) - return self._consumer - - def create_consumer(self, store=None): - return Consumer(self.strategy.openid_session_dict(SESSION_NAME), store) - - def uses_redirect(self): - """Return true if openid request will be handled with redirect or - HTML content will be returned. - """ - return self.openid_request().shouldSendRedirect() - - def openid_request(self, params=None): - """Return openid request""" - try: - return self.consumer().begin(url_add_parameters(self.openid_url(), - params)) - except DiscoveryFailure as err: - raise AuthException(self, 'OpenID discovery error: {0}'.format( - err - )) - - def openid_url(self): - """Return service provider URL. - This base class is generic accepting a POST parameter that specifies - provider URL.""" - if self.URL: - return self.URL - elif OPENID_ID_FIELD in self.data: - return self.data[OPENID_ID_FIELD] - else: - raise AuthMissingParameter(self, OPENID_ID_FIELD) - - -class OpenIdConnectAssociation(object): - """ Use Association model to save the nonce by force. """ - - def __init__(self, handle, secret='', issued=0, lifetime=0, assoc_type=''): - self.handle = handle # as nonce - self.secret = secret.encode() # not use - self.issued = issued # not use - self.lifetime = lifetime # not use - self.assoc_type = assoc_type # as state - - -class OpenIdConnectAuth(BaseOAuth2): - """ - Base class for Open ID Connect backends. - - Currently only the code response type is supported. - """ - ID_TOKEN_ISSUER = None - ID_TOKEN_MAX_AGE = 600 - DEFAULT_SCOPE = ['openid'] - EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')] - # Set after access_token is retrieved - id_token = None - - def auth_params(self, state=None): - """Return extra arguments needed on auth process.""" - params = super(OpenIdConnectAuth, self).auth_params(state) - params['nonce'] = self.get_and_store_nonce( - self.AUTHORIZATION_URL, state - ) - return params - - def auth_complete_params(self, state=None): - params = super(OpenIdConnectAuth, self).auth_complete_params(state) - # Add a nonce to the request so that to help counter CSRF - params['nonce'] = self.get_and_store_nonce( - self.ACCESS_TOKEN_URL, state - ) - return params - - def get_and_store_nonce(self, url, state): - # Create a nonce - nonce = self.strategy.random_string(64) - # Store the nonce - association = OpenIdConnectAssociation(nonce, assoc_type=state) - self.strategy.storage.association.store(url, association) - return nonce - - def get_nonce(self, nonce): - try: - return self.strategy.storage.association.get( - server_url=self.ACCESS_TOKEN_URL, - handle=nonce - )[0] - except IndexError: - pass - - def remove_nonce(self, nonce_id): - self.strategy.storage.association.remove([nonce_id]) - - def validate_and_return_id_token(self, id_token): - """ - Validates the id_token according to the steps at - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. - """ - client_id, _client_secret = self.get_key_and_secret() - - decode_kwargs = { - 'algorithms': ['HS256'], - 'audience': client_id, - 'issuer': self.ID_TOKEN_ISSUER, - 'key': self.setting('ID_TOKEN_DECRYPTION_KEY'), - 'options': { - 'verify_signature': True, - 'verify_exp': True, - 'verify_iat': True, - 'verify_aud': True, - 'verify_iss': True, - 'require_exp': True, - 'require_iat': True, - }, - } - decode_kwargs.update(self.setting('ID_TOKEN_JWT_DECODE_KWARGS', {})) - - try: - # Decode the JWT and raise an error if the secret is invalid or - # the response has expired. - id_token = jwt_decode(id_token, **decode_kwargs) - except InvalidTokenError as err: - raise AuthTokenError(self, err) - - # Verify the token was issued within a specified amount of time - iat_leeway = self.setting('ID_TOKEN_MAX_AGE', self.ID_TOKEN_MAX_AGE) - utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) - if id_token['iat'] < (utc_timestamp - iat_leeway): - raise AuthTokenError(self, 'Incorrect id_token: iat') - - # Validate the nonce to ensure the request was not modified - nonce = id_token.get('nonce') - if not nonce: - raise AuthTokenError(self, 'Incorrect id_token: nonce') - - nonce_obj = self.get_nonce(nonce) - if nonce_obj: - self.remove_nonce(nonce_obj.id) - else: - raise AuthTokenError(self, 'Incorrect id_token: nonce') - return id_token - - def request_access_token(self, *args, **kwargs): - """ - Retrieve the access token. Also, validate the id_token and - store it (temporarily). - """ - response = self.get_json(*args, **kwargs) - self.id_token = self.validate_and_return_id_token(response['id_token']) - return response +from social_core.backends.open_id import OpenIdAuth, \ + OpenIdConnectAssociation, OpenIdConnectAuth diff --git a/social/backends/openstreetmap.py b/social/backends/openstreetmap.py index be3bc542a..a50344ba4 100644 --- a/social/backends/openstreetmap.py +++ b/social/backends/openstreetmap.py @@ -1,57 +1 @@ -""" -OpenStreetMap OAuth support. - -This adds support for OpenStreetMap OAuth service. An application must be -registered first on OpenStreetMap and the settings -SOCIAL_AUTH_OPENSTREETMAP_KEY and SOCIAL_AUTH_OPENSTREETMAP_SECRET -must be defined with the corresponding values. - -More info: http://wiki.openstreetmap.org/wiki/OAuth -""" -from xml.dom import minidom - -from social.backends.oauth import BaseOAuth1 - - -class OpenStreetMapOAuth(BaseOAuth1): - """OpenStreetMap OAuth authentication backend""" - name = 'openstreetmap' - AUTHORIZATION_URL = 'http://www.openstreetmap.org/oauth/authorize' - REQUEST_TOKEN_URL = 'http://www.openstreetmap.org/oauth/request_token' - ACCESS_TOKEN_URL = 'http://www.openstreetmap.org/oauth/access_token' - EXTRA_DATA = [ - ('id', 'id'), - ('avatar', 'avatar'), - ('account_created', 'account_created') - ] - - def get_user_details(self, response): - """Return user details from OpenStreetMap account""" - return { - 'username': response['username'], - 'email': '', - 'fullname': '', - 'first_name': '', - 'last_name': '' - } - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - response = self.oauth_request( - access_token, 'http://api.openstreetmap.org/api/0.6/user/details' - ) - try: - dom = minidom.parseString(response.content) - except ValueError: - return None - user = dom.getElementsByTagName('user')[0] - try: - avatar = dom.getElementsByTagName('img')[0].getAttribute('href') - except IndexError: - avatar = None - return { - 'id': user.getAttribute('id'), - 'username': user.getAttribute('display_name'), - 'account_created': user.getAttribute('account_created'), - 'avatar': avatar - } +from social_core.backends.openstreetmap import OpenStreetMapOAuth diff --git a/social/backends/orbi.py b/social/backends/orbi.py index ab6c13ecb..bc4f01067 100644 --- a/social/backends/orbi.py +++ b/social/backends/orbi.py @@ -1,42 +1 @@ -""" -Orbi OAuth2 backend -""" -from social.backends.oauth import BaseOAuth2 - - -class OrbiOAuth2(BaseOAuth2): - """Orbi OAuth2 authentication backend""" - name = 'orbi' - AUTHORIZATION_URL = 'https://login.orbi.kr/oauth/authorize' - ACCESS_TOKEN_URL = 'https://login.orbi.kr/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('imin', 'imin'), - ('nick', 'nick'), - ('photo', 'photo'), - ('sex', 'sex'), - ('birth', 'birth'), - ] - - def get_user_id(self, details, response): - return response.get('id') - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - response.get('name', ''), - response.get('first_name', ''), - response.get('last_name', '') - ) - return { - 'username': response.get('username', response.get('name')), - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - } - - def user_data(self, access_token, *args, **kwargs): - """Load user data from orbi""" - return self.get_json('https://login.orbi.kr/oauth/user/get', params={ - 'access_token': access_token - }) +from social_core.backends.orbi import OrbiOAuth2 diff --git a/social/backends/persona.py b/social/backends/persona.py index 3c288e450..9e916b70d 100644 --- a/social/backends/persona.py +++ b/social/backends/persona.py @@ -1,50 +1 @@ -""" -Mozilla Persona authentication backend, docs at: - http://psa.matiasaguirre.net/docs/backends/persona.html -""" -from social.utils import handle_http_errors -from social.backends.base import BaseAuth -from social.exceptions import AuthFailed, AuthMissingParameter - - -class PersonaAuth(BaseAuth): - """BrowserID authentication backend""" - name = 'persona' - - def get_user_id(self, details, response): - """Use BrowserID email as ID""" - return details['email'] - - def get_user_details(self, response): - """Return user details, BrowserID only provides Email.""" - # {'status': 'okay', - # 'audience': 'localhost:8000', - # 'expires': 1328983575529, - # 'email': 'name@server.com', - # 'issuer': 'browserid.org'} - email = response['email'] - return {'username': email.split('@', 1)[0], - 'email': email, - 'fullname': '', - 'first_name': '', - 'last_name': ''} - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return users extra data""" - return {'audience': response['audience'], - 'issuer': response['issuer']} - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - if 'assertion' not in self.data: - raise AuthMissingParameter(self, 'assertion') - - response = self.get_json('https://browserid.org/verify', data={ - 'assertion': self.data['assertion'], - 'audience': self.strategy.request_host() - }, method='POST') - if response.get('status') == 'failure': - raise AuthFailed(self) - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) +from social_core.backends.persona import PersonaAuth diff --git a/social/backends/pinterest.py b/social/backends/pinterest.py index f14563374..3e730edf5 100644 --- a/social/backends/pinterest.py +++ b/social/backends/pinterest.py @@ -1,46 +1 @@ -# -*- coding: utf-8 -*- -""" -Pinterest OAuth2 backend, docs at: - https://developers.pinterest.com/docs/api/authentication/ -""" - -from __future__ import unicode_literals - -import ssl - -from social.backends.oauth import BaseOAuth2 - - -class PinterestOAuth2(BaseOAuth2): - name = 'pinterest' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://api.pinterest.com/oauth/' - ACCESS_TOKEN_URL = 'https://api.pinterest.com/v1/oauth/token' - REDIRECT_STATE = False - ACCESS_TOKEN_METHOD = 'POST' - SSL_PROTOCOL = ssl.PROTOCOL_TLSv1 - - def user_data(self, access_token, *args, **kwargs): - response = self.get_json('https://api.pinterest.com/v1/me/', - params={'access_token': access_token}) - - if 'data' in response: - username = response['data']['url'].strip('/').split('/')[-1] - response = { - 'user_id': response['data']['id'], - 'first_name': response['data']['first_name'], - 'last_name': response['data']['last_name'], - 'username': username, - } - return response - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - first_name=response['first_name'], - last_name=response['last_name']) - - return {'username': response.get('username'), - 'email': None, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} +from social_core.backends.pinterest import PinterestOAuth2 diff --git a/social/backends/pixelpin.py b/social/backends/pixelpin.py index 3a1ed2b3f..7c098fcf1 100644 --- a/social/backends/pixelpin.py +++ b/social/backends/pixelpin.py @@ -1,33 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class PixelPinOAuth2(BaseOAuth2): - """PixelPin OAuth authentication backend""" - name = 'pixelpin-oauth2' - ID_KEY = 'id' - AUTHORIZATION_URL = 'https://login.pixelpin.co.uk/OAuth2/Flogin.aspx' - ACCESS_TOKEN_URL = 'https://ws3.pixelpin.co.uk/index.php/api/token' - ACCESS_TOKEN_METHOD = 'POST' - REQUIRES_EMAIL_VALIDATION = False - EXTRA_DATA = [ - ('id', 'id'), - ] - - def get_user_details(self, response): - """Return user details from PixelPin account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('firstName'), - last_name=response.get('lastName') - ) - return {'username': response.get('firstName'), - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://ws3.pixelpin.co.uk/index.php/api/userdata', - params={'access_token': access_token} - ) +from social_core.backends.pixelpin import PixelPinOAuth2 diff --git a/social/backends/pocket.py b/social/backends/pocket.py index 49b73d55a..e291c4fe7 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -1,45 +1 @@ -""" -Pocket OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/pocket.html -""" -from social.backends.base import BaseAuth -from social.utils import handle_http_errors - - -class PocketAuth(BaseAuth): - name = 'pocket' - AUTHORIZATION_URL = 'https://getpocket.com/auth/authorize' - ACCESS_TOKEN_URL = 'https://getpocket.com/v3/oauth/authorize' - REQUEST_TOKEN_URL = 'https://getpocket.com/v3/oauth/request' - ID_KEY = 'username' - - def get_json(self, url, *args, **kwargs): - headers = {'X-Accept': 'application/json'} - kwargs.update({'method': 'POST', 'headers': headers}) - return super(PocketAuth, self).get_json(url, *args, **kwargs) - - def get_user_details(self, response): - return {'username': response['username']} - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - return response - - def auth_url(self): - data = { - 'consumer_key': self.setting('KEY'), - 'redirect_uri': self.redirect_uri, - } - token = self.get_json(self.REQUEST_TOKEN_URL, data=data)['code'] - self.strategy.session_set('pocket_request_token', token) - bits = (self.AUTHORIZATION_URL, token, self.redirect_uri) - return '%s?request_token=%s&redirect_uri=%s' % bits - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - data = { - 'consumer_key': self.setting('KEY'), - 'code': self.strategy.session_get('pocket_request_token'), - } - response = self.get_json(self.ACCESS_TOKEN_URL, data=data) - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) +from social_core.backends.pocket import PocketAuth diff --git a/social/backends/podio.py b/social/backends/podio.py index 1c4b0df44..782bd4ac8 100644 --- a/social/backends/podio.py +++ b/social/backends/podio.py @@ -1,38 +1 @@ -""" -Podio OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/podio.html -""" -from social.backends.oauth import BaseOAuth2 - - -class PodioOAuth2(BaseOAuth2): - """Podio OAuth authentication backend""" - name = 'podio' - AUTHORIZATION_URL = 'https://podio.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://podio.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('access_token', 'access_token'), - ('token_type', 'token_type'), - ('expires_in', 'expires'), - ('refresh_token', 'refresh_token'), - ] - - def get_user_id(self, details, response): - return response['ref']['id'] - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - response['profile']['name'] - ) - return { - 'username': 'user_%d' % response['user']['user_id'], - 'email': response['user']['mail'], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - } - - def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://api.podio.com/user/status', - headers={'Authorization': 'OAuth2 ' + access_token}) +from social_core.backends.podio import PodioOAuth2 diff --git a/social/backends/professionali.py b/social/backends/professionali.py index 5e79ba96b..958fa1b09 100644 --- a/social/backends/professionali.py +++ b/social/backends/professionali.py @@ -1,55 +1 @@ -# -*- coding: utf-8 -*- -""" -Professionaly OAuth 2.0 support. - -This contribution adds support for professionaly.ru OAuth 2.0. -Username is retrieved from the identity returned by server. -""" -from time import time - -from social.utils import parse_qs -from social.backends.oauth import BaseOAuth2 - - -class ProfessionaliOAuth2(BaseOAuth2): - name = 'professionali' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://api.professionali.ru/oauth/authorize.html' - ACCESS_TOKEN_URL = 'https://api.professionali.ru/oauth/getToken.json' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('avatar_big', 'avatar_big'), - ('link', 'link') - ] - - def get_user_details(self, response): - first_name, last_name = map(response.get, ('firstname', 'lastname')) - email = '' - if self.setting('FAKE_EMAIL'): - email = '{0}@professionali.ru'.format(time()) - return { - 'username': '{0}_{1}'.format(last_name, first_name), - 'first_name': first_name, - 'last_name': last_name, - 'email': email - } - - def user_data(self, access_token, response, *args, **kwargs): - url = 'https://api.professionali.ru/v6/users/get.json' - fields = list(set(['firstname', 'lastname', 'avatar_big', 'link'] + - self.setting('EXTRA_DATA', []))) - params = { - 'fields': ','.join(fields), - 'access_token': access_token, - 'ids[]': response['user_id'] - } - try: - return self.get_json(url, params)[0] - except (TypeError, KeyError, IOError, ValueError, IndexError): - return None - - def get_json(self, url, *args, **kwargs): - return self.request(url, verify=False, *args, **kwargs).json() - - def get_querystring(self, url, *args, **kwargs): - return parse_qs(self.request(url, verify=False, *args, **kwargs).text) +from social_core.backends.professionali import ProfessionaliOAuth2 diff --git a/social/backends/pushbullet.py b/social/backends/pushbullet.py index 4899ab901..1d6c682c5 100644 --- a/social/backends/pushbullet.py +++ b/social/backends/pushbullet.py @@ -1,23 +1 @@ -import base64 - -from social.backends.oauth import BaseOAuth2 - - -class PushbulletOAuth2(BaseOAuth2): - """pushbullet OAuth authentication backend""" - name = 'pushbullet' - EXTRA_DATA = [('id', 'id')] - ID_KEY = 'username' - AUTHORIZATION_URL = 'https://www.pushbullet.com/authorize' - REQUEST_TOKEN_URL = 'https://api.pushbullet.com/oauth2/token' - ACCESS_TOKEN_URL = 'https://api.pushbullet.com/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - STATE_PARAMETER = False - - def get_user_details(self, response): - return {'username': response.get('access_token')} - - def get_user_id(self, details, response): - auth = 'Basic {0}'.format(base64.b64encode(details['username'])) - return self.get_json('https://api.pushbullet.com/v2/users/me', - headers={'Authorization': auth})['iden'] +from social_core.backends.pushbullet import PushbulletOAuth2 diff --git a/social/backends/qiita.py b/social/backends/qiita.py index 92bcbd38e..22138e648 100644 --- a/social/backends/qiita.py +++ b/social/backends/qiita.py @@ -1,66 +1 @@ -""" -Qiita OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/qiita.html - http://qiita.com/api/v2/docs#get-apiv2oauthauthorize -""" -import json - -from social.backends.oauth import BaseOAuth2 - - -class QiitaOAuth2(BaseOAuth2): - """Qiita OAuth authentication backend""" - name = 'qiita' - - AUTHORIZATION_URL = 'https://qiita.com/api/v2/oauth/authorize' - ACCESS_TOKEN_URL = 'https://qiita.com/api/v2/access_tokens' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ' ' - REDIRECT_STATE = True - EXTRA_DATA = [ - ('description', 'description'), - ('facebook_id', 'facebook_id'), - ('followees_count', 'followees_count'), - ('followers_count', 'followers_count'), - ('github_login_name', 'github_login_name'), - ('id', 'id'), - ('items_count', 'items_count'), - ('linkedin_id', 'linkedin_id'), - ('location', 'location'), - ('name', 'name'), - ('organization', 'organization'), - ('profile_image_url', 'profile_image_url'), - ('twitter_screen_name', 'twitter_screen_name'), - ('website_url', 'website_url'), - ] - - def auth_complete_params(self, state=None): - data = super(QiitaOAuth2, self).auth_complete_params(state) - if "grant_type" in data: - del data["grant_type"] - if "redirect_uri" in data: - del data["redirect_uri"] - return json.dumps(data) - - def auth_headers(self): - return {'Content-Type': 'application/json'} - - def request_access_token(self, *args, **kwargs): - data = super(QiitaOAuth2, self).request_access_token(*args, **kwargs) - data.update({'access_token': data['token']}) - return data - - def get_user_details(self, response): - """Return user details from Qiita account""" - return { - 'username': response['id'], - 'fullname': response['name'], - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://qiita.com/api/v2/authenticated_user', - headers={ - 'Authorization': 'Bearer {0}'.format(access_token) - }) +from social_core.backends.qiita import QiitaOAuth2 diff --git a/social/backends/qq.py b/social/backends/qq.py index 34f36b321..950a8c345 100644 --- a/social/backends/qq.py +++ b/social/backends/qq.py @@ -1,70 +1 @@ -""" -Created on May 13, 2014 - -@author: Yong Zhang (zyfyfe@gmail.com) -""" - -import json - -from social.utils import parse_qs -from social.backends.oauth import BaseOAuth2 - - -class QQOAuth2(BaseOAuth2): - name = 'qq' - ID_KEY = 'openid' - AUTHORIZE_URL = 'https://graph.qq.com/oauth2.0/authorize' - ACCESS_TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' - AUTHORIZATION_URL = 'https://graph.qq.com/oauth2.0/authorize' - OPENID_URL = 'https://graph.qq.com/oauth2.0/me' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('nickname', 'username'), - ('figureurl_qq_1', 'profile_image_url'), - ('gender', 'gender') - ] - - def get_user_details(self, response): - """ - Return user detail from QQ account sometimes nickname will duplicate - with another qq account, to avoid this issue it's possible to use - openid as username. - """ - if self.setting('USE_OPENID_AS_USERNAME', False): - username = response.get('openid', '') - else: - username = response.get('nickname', '') - - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('nickname', '') - ) - - return { - 'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def get_openid(self, access_token): - response = self.request(self.OPENID_URL, params={ - 'access_token': access_token - }) - data = json.loads(response.content[10:-3]) - return data['openid'] - - def user_data(self, access_token, *args, **kwargs): - openid = self.get_openid(access_token) - response = self.get_json( - 'https://graph.qq.com/user/get_user_info', params={ - 'access_token': access_token, - 'oauth_consumer_key': self.setting('SOCIAL_AUTH_QQ_KEY'), - 'openid': openid - } - ) - response['openid'] = openid - return response - - def request_access_token(self, url, data, *args, **kwargs): - response = self.request(url, params=data, *args, **kwargs) - return parse_qs(response.content) +from social_core.backends.qq import QQOAuth2 diff --git a/social/backends/rdio.py b/social/backends/rdio.py index e4af55164..13ca0d462 100644 --- a/social/backends/rdio.py +++ b/social/backends/rdio.py @@ -1,72 +1 @@ -""" -Rdio OAuth1 and OAuth2 backends, docs at: - http://psa.matiasaguirre.net/docs/backends/rdio.html -""" -from social.backends.oauth import BaseOAuth1, BaseOAuth2, OAuthAuth - - -RDIO_API = 'https://www.rdio.com/api/1/' - - -class BaseRdio(OAuthAuth): - ID_KEY = 'key' - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - fullname=response['displayName'], - first_name=response['firstName'], - last_name=response['lastName'] - ) - return { - 'username': response['username'], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - -class RdioOAuth1(BaseRdio, BaseOAuth1): - """Rdio OAuth authentication backend""" - name = 'rdio-oauth1' - REQUEST_TOKEN_URL = 'http://api.rdio.com/oauth/request_token' - AUTHORIZATION_URL = 'https://www.rdio.com/oauth/authorize' - ACCESS_TOKEN_URL = 'http://api.rdio.com/oauth/access_token' - EXTRA_DATA = [ - ('key', 'rdio_id'), - ('icon', 'rdio_icon_url'), - ('url', 'rdio_profile_url'), - ('username', 'rdio_username'), - ('streamRegion', 'rdio_stream_region'), - ] - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - params = {'method': 'currentUser', - 'extras': 'username,displayName,streamRegion'} - request = self.oauth_request(access_token, RDIO_API, - params, method='POST') - return self.get_json(request.url, method='POST', - data=request.to_postdata())['result'] - - -class RdioOAuth2(BaseRdio, BaseOAuth2): - name = 'rdio-oauth2' - AUTHORIZATION_URL = 'https://www.rdio.com/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://www.rdio.com/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('key', 'rdio_id'), - ('icon', 'rdio_icon_url'), - ('url', 'rdio_profile_url'), - ('username', 'rdio_username'), - ('streamRegion', 'rdio_stream_region'), - ('refresh_token', 'refresh_token', True), - ('token_type', 'token_type', True), - ] - - def user_data(self, access_token, *args, **kwargs): - return self.get_json(RDIO_API, method='POST', data={ - 'method': 'currentUser', - 'extras': 'username,displayName,streamRegion', - 'access_token': access_token - })['result'] +from social_core.backends.rdio import BaseRdio, RdioOAuth1, RdioOAuth2 diff --git a/social/backends/readability.py b/social/backends/readability.py index 76e241f15..2fc458f91 100644 --- a/social/backends/readability.py +++ b/social/backends/readability.py @@ -1,35 +1 @@ -""" -Readability OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/readability.html -""" -from social.backends.oauth import BaseOAuth1 - - -READABILITY_API = 'https://www.readability.com/api/rest/v1' - - -class ReadabilityOAuth(BaseOAuth1): - """Readability OAuth authentication backend""" - name = 'readability' - ID_KEY = 'username' - AUTHORIZATION_URL = '{0}/oauth/authorize/'.format(READABILITY_API) - REQUEST_TOKEN_URL = '{0}/oauth/request_token/'.format(READABILITY_API) - ACCESS_TOKEN_URL = '{0}/oauth/access_token/'.format(READABILITY_API) - EXTRA_DATA = [('date_joined', 'date_joined'), - ('kindle_email_address', 'kindle_email_address'), - ('avatar_url', 'avatar_url'), - ('email_into_address', 'email_into_address')] - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - first_name=response['first_name'], - last_name=response['last_name'] - ) - return {'username': response['username'], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token): - return self.get_json(READABILITY_API + '/users/_current', - auth=self.oauth_auth(access_token)) +from social_core.backends.readability import ReadabilityOAuth diff --git a/social/backends/reddit.py b/social/backends/reddit.py index 712bdfb76..44df673bc 100644 --- a/social/backends/reddit.py +++ b/social/backends/reddit.py @@ -1,53 +1 @@ -""" -Reddit OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/reddit.html -""" -import base64 - -from social.backends.oauth import BaseOAuth2 - - -class RedditOAuth2(BaseOAuth2): - """Reddit OAuth2 authentication backend""" - name = 'reddit' - AUTHORIZATION_URL = 'https://ssl.reddit.com/api/v1/authorize' - ACCESS_TOKEN_URL = 'https://ssl.reddit.com/api/v1/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REFRESH_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - SCOPE_SEPARATOR = ',' - DEFAULT_SCOPE = ['identity'] - SEND_USER_AGENT = True - EXTRA_DATA = [ - ('id', 'id'), - ('name', 'username'), - ('link_karma', 'link_karma'), - ('comment_karma', 'comment_karma'), - ('refresh_token', 'refresh_token'), - ('expires_in', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Reddit account""" - return {'username': response.get('name'), - 'email': '', 'fullname': '', - 'first_name': '', 'last_name': ''} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://oauth.reddit.com/api/v1/me.json', - headers={'Authorization': 'bearer ' + access_token} - ) - - def auth_headers(self): - return { - 'Authorization': b'Basic ' + base64.urlsafe_b64encode( - '{0}:{1}'.format(*self.get_key_and_secret()).encode() - ) - } - - def refresh_token_params(self, token, redirect_uri=None, *args, **kwargs): - params = super(RedditOAuth2, self).refresh_token_params(token) - params['redirect_uri'] = self.redirect_uri or redirect_uri - return params +from social_core.backends.reddit import RedditOAuth2 diff --git a/social/backends/runkeeper.py b/social/backends/runkeeper.py index d7a2f3bf5..f5c1ed441 100644 --- a/social/backends/runkeeper.py +++ b/social/backends/runkeeper.py @@ -1,47 +1 @@ -""" -RunKeeper OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/runkeeper.html -""" -from social.backends.oauth import BaseOAuth2 - - -class RunKeeperOAuth2(BaseOAuth2): - """RunKeeper OAuth authentication backend""" - name = 'runkeeper' - AUTHORIZATION_URL = 'https://runkeeper.com/apps/authorize' - ACCESS_TOKEN_URL = 'https://runkeeper.com/apps/token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('userID', 'id'), - ] - - def get_user_id(self, details, response): - return response['userID'] - - def get_user_details(self, response): - """Parse username from profile link""" - username = None - profile_url = response.get('profile') - if len(profile_url): - profile_url_parts = profile_url.split('http://runkeeper.com/user/') - if len(profile_url_parts) > 1 and len(profile_url_parts[1]): - username = profile_url_parts[1] - fullname, first_name, last_name = self.get_user_names( - fullname=response.get('name') - ) - return {'username': username, - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - # We need to use the /user endpoint to get the user id, the /profile - # endpoint contains name, user name, location, gender - user_data = self._user_data(access_token, '/user') - profile_data = self._user_data(access_token, '/profile') - return dict(user_data, **profile_data) - - def _user_data(self, access_token, path): - url = 'https://api.runkeeper.com{0}'.format(path) - return self.get_json(url, params={'access_token': access_token}) +from social_core.backends.runkeeper import RunKeeperOAuth2 diff --git a/social/backends/salesforce.py b/social/backends/salesforce.py index a77aa854a..1da8afdb3 100644 --- a/social/backends/salesforce.py +++ b/social/backends/salesforce.py @@ -1,48 +1,2 @@ -from social.backends.oauth import BaseOAuth2 -from social.p3 import urlencode - - -class SalesforceOAuth2(BaseOAuth2): - """Salesforce OAuth2 authentication backend""" - name = 'salesforce-oauth2' - AUTHORIZATION_URL = \ - 'https://login.salesforce.com/services/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token' - REVOKE_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/revoke' - ACCESS_TOKEN_METHOD = 'POST' - REFRESH_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ' ' - EXTRA_DATA = [ - ('id', 'id'), - ('instance_url', 'instance_url'), - ('issued_at', 'issued_at'), - ('signature', 'signature'), - ('refresh_token', 'refresh_token'), - ] - - def get_user_details(self, response): - """Return user details from a Salesforce account""" - return { - 'username': response.get('username'), - 'email': response.get('email') or '', - 'first_name': response.get('first_name'), - 'last_name': response.get('last_name'), - 'fullname': response.get('display_name') - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - user_id_url = kwargs.get('response').get('id') - url = user_id_url + '?' + urlencode({'access_token': access_token}) - try: - return self.get_json(url) - except ValueError: - return None - - -class SalesforceOAuth2Sandbox(SalesforceOAuth2): - """Salesforce OAuth2 authentication testing backend""" - name = 'salesforce-oauth2-sandbox' - AUTHORIZATION_URL = 'https://test.salesforce.com/services/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://test.salesforce.com/services/oauth2/token' - REVOKE_TOKEN_URL = 'https://test.salesforce.com/services/oauth2/revoke' +from social_core.backends.salesforce import SalesforceOAuth2, \ + SalesforceOAuth2Sandbox diff --git a/social/backends/saml.py b/social/backends/saml.py index a249c0b40..30115bc0f 100644 --- a/social/backends/saml.py +++ b/social/backends/saml.py @@ -1,326 +1,2 @@ -""" -Backend for SAML 2.0 support - -Terminology: - -"Service Provider" (SP): Your web app -"Identity Provider" (IdP): The third-party site that is authenticating - users via SAML -""" -from onelogin.saml2.auth import OneLogin_Saml2_Auth -from onelogin.saml2.settings import OneLogin_Saml2_Settings - -from social.backends.base import BaseAuth -from social.exceptions import AuthFailed, AuthMissingParameter - -# Helpful constants: -OID_COMMON_NAME = "urn:oid:2.5.4.3" -OID_EDU_PERSON_PRINCIPAL_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" -OID_EDU_PERSON_ENTITLEMENT = "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" -OID_GIVEN_NAME = "urn:oid:2.5.4.42" -OID_MAIL = "urn:oid:0.9.2342.19200300.100.1.3" -OID_SURNAME = "urn:oid:2.5.4.4" -OID_USERID = "urn:oid:0.9.2342.19200300.100.1.1" - - -class SAMLIdentityProvider(object): - """Wrapper around configuration for a SAML Identity provider""" - def __init__(self, name, **kwargs): - """Load and parse configuration""" - self.name = name - # name should be a slug and must not contain a colon, which - # could conflict with uid prefixing: - assert ':' not in self.name and ' ' not in self.name, \ - 'IdP "name" should be a slug (short, no spaces)' - self.conf = kwargs - - def get_user_permanent_id(self, attributes): - """ - The most important method: Get a permanent, unique identifier - for this user from the attributes supplied by the IdP. - - If you want to use the NameID, it's available via - attributes['name_id'] - """ - return attributes[ - self.conf.get('attr_user_permanent_id', OID_USERID) - ][0] - - # Attributes processing: - def get_user_details(self, attributes): - """ - Given the SAML attributes extracted from the SSO response, get - the user data like name. - """ - return { - 'fullname': self.get_attr(attributes, 'attr_full_name', - OID_COMMON_NAME), - 'first_name': self.get_attr(attributes, 'attr_first_name', - OID_GIVEN_NAME), - 'last_name': self.get_attr(attributes, 'attr_last_name', - OID_SURNAME), - 'username': self.get_attr(attributes, 'attr_username', - OID_USERID), - 'email': self.get_attr(attributes, 'attr_email', - OID_MAIL), - } - - def get_attr(self, attributes, conf_key, default_attribute): - """ - Internal helper method. - Get the attribute 'default_attribute' out of the attributes, - unless self.conf[conf_key] overrides the default by specifying - another attribute to use. - """ - key = self.conf.get(conf_key, default_attribute) - return attributes[key][0] if key in attributes else None - - @property - def entity_id(self): - """Get the entity ID for this IdP""" - # Required. e.g. "https://idp.testshib.org/idp/shibboleth" - return self.conf['entity_id'] - - @property - def sso_url(self): - """Get the SSO URL for this IdP""" - # Required. e.g. - # "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" - return self.conf['url'] - - @property - def x509cert(self): - """X.509 Public Key Certificate for this IdP""" - return self.conf['x509cert'] - - @property - def saml_config_dict(self): - """Get the IdP configuration dict in the format required by - python-saml""" - return { - 'entityId': self.entity_id, - 'singleSignOnService': { - 'url': self.sso_url, - # python-saml only supports Redirect - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - }, - 'x509cert': self.x509cert, - } - - -class DummySAMLIdentityProvider(SAMLIdentityProvider): - """ - A placeholder IdP used when we must specify something, e.g. when - generating SP metadata. - - If OneLogin_Saml2_Auth is modified to not always require IdP - config, this can be removed. - """ - def __init__(self): - super(DummySAMLIdentityProvider, self).__init__( - 'dummy', - entity_id='https://dummy.none/saml2', - url='https://dummy.none/SSO', - x509cert='' - ) - - -class SAMLAuth(BaseAuth): - """ - PSA Backend that implements SAML 2.0 Service Provider (SP) functionality. - - Unlike all of the other backends, this one can be configured to work with - many identity providers (IdPs). For example, a University that belongs to a - Shibboleth federation may support authentication via ~100 partner - universities. Also, the IdP configuration can be changed at runtime if you - require that functionality - just subclass this and override `get_idp()`. - - Several settings are required. Here's an example: - - SOCIAL_AUTH_SAML_SP_ENTITY_ID = "https://saml.example.com/" - SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = "... X.509 certificate string ..." - SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = "... private key ..." - SOCIAL_AUTH_SAML_ORG_INFO = { - "en-US": { - "name": "example", - "displayname": "Example Inc.", - "url": "http://example.com" - } - } - SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = { - "givenName": "Tech Gal", - "emailAddress": "technical@example.com" - } - SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { - "givenName": "Support Guy", - "emailAddress": "support@example.com" - } - SOCIAL_AUTH_SAML_ENABLED_IDPS = { - "testshib": { - "entity_id": "https://idp.testshib.org/idp/shibboleth", - "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", - "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0B... - ...8Bbnl+ev0peYzxFyF5sQA==", - } - } - - Optional settings: - SOCIAL_AUTH_SAML_SP_EXTRA = {} - SOCIAL_AUTH_SAML_SECURITY_CONFIG = {} - """ - name = "saml" - - def get_idp(self, idp_name): - """Given the name of an IdP, get a SAMLIdentityProvider instance""" - idp_config = self.setting('ENABLED_IDPS')[idp_name] - return SAMLIdentityProvider(idp_name, **idp_config) - - def generate_saml_config(self, idp): - """ - Generate the configuration required to instantiate OneLogin_Saml2_Auth - """ - # The shared absolute URL that all IdPs redirect back to - - # this is specified in our metadata.xml: - abs_completion_url = self.redirect_uri - config = { - 'contactPerson': { - 'technical': self.setting('TECHNICAL_CONTACT'), - 'support': self.setting('SUPPORT_CONTACT') - }, - 'debug': True, - 'idp': idp.saml_config_dict, - 'organization': self.setting('ORG_INFO'), - 'security': { - 'metadataValidUntil': '', - 'metadataCacheDuration': 'P10D', # metadata valid for ten days - }, - 'sp': { - 'assertionConsumerService': { - 'url': abs_completion_url, - # python-saml only supports HTTP-POST - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - }, - 'entityId': self.setting('SP_ENTITY_ID'), - 'x509cert': self.setting('SP_PUBLIC_CERT'), - 'privateKey': self.setting('SP_PRIVATE_KEY'), - }, - 'strict': True, # We must force strict mode - for security - } - config["security"].update(self.setting("SECURITY_CONFIG", {})) - config["sp"].update(self.setting("SP_EXTRA", {})) - return config - - def generate_metadata_xml(self): - """ - Helper method that can be used from your web app to generate the XML - metadata required to link your web app as a Service Provider with - each IdP you wish to use. - - Returns (metadata XML string, list of errors) - - Example usage (Django): - from social.apps.django_app.utils import load_strategy, \ - load_backend - def saml_metadata_view(request): - complete_url = reverse('social:complete', args=("saml", )) - saml_backend = load_backend(load_strategy(request), "saml", - complete_url) - metadata, errors = saml_backend.generate_metadata_xml() - if not errors: - return HttpResponse(content=metadata, - content_type='text/xml') - return HttpResponseServerError(content=', '.join(errors)) - """ - # python-saml requires us to specify something here even - # though it's not used - idp = DummySAMLIdentityProvider() - config = self.generate_saml_config(idp) - saml_settings = OneLogin_Saml2_Settings(config) - metadata = saml_settings.get_sp_metadata() - errors = saml_settings.validate_metadata(metadata) - return metadata, errors - - def _create_saml_auth(self, idp): - """Get an instance of OneLogin_Saml2_Auth""" - config = self.generate_saml_config(idp) - request_info = { - 'https': 'on' if self.strategy.request_is_secure() else 'off', - 'http_host': self.strategy.request_host(), - 'script_name': self.strategy.request_path(), - 'server_port': self.strategy.request_port(), - 'get_data': self.strategy.request_get(), - 'post_data': self.strategy.request_post(), - } - return OneLogin_Saml2_Auth(request_info, config) - - def auth_url(self): - """Get the URL to which we must redirect in order to - authenticate the user""" - try: - idp_name = self.strategy.request_data()['idp'] - except KeyError: - raise AuthMissingParameter(self, 'idp') - auth = self._create_saml_auth(idp=self.get_idp(idp_name)) - # Below, return_to sets the RelayState, which can contain - # arbitrary data. We use it to store the specific SAML IdP - # name, since we multiple IdPs share the same auth_complete - # URL. - return auth.login(return_to=idp_name) - - def get_user_details(self, response): - """Get user details like full name, email, etc. from the - response - see auth_complete""" - idp = self.get_idp(response['idp_name']) - return idp.get_user_details(response['attributes']) - - def get_user_id(self, details, response): - """ - Get the permanent ID for this user from the response. - We prefix each ID with the name of the IdP so that we can - connect multiple IdPs to this user. - """ - idp = self.get_idp(response['idp_name']) - uid = idp.get_user_permanent_id(response['attributes']) - return '{0}:{1}'.format(idp.name, uid) - - def auth_complete(self, *args, **kwargs): - """ - The user has been redirected back from the IdP and we should - now log them in, if everything checks out. - """ - idp_name = self.strategy.request_data()['RelayState'] - idp = self.get_idp(idp_name) - auth = self._create_saml_auth(idp) - auth.process_response() - errors = auth.get_errors() - if errors or not auth.is_authenticated(): - reason = auth.get_last_error_reason() - raise AuthFailed( - self, 'SAML login failed: {0} ({1})'.format(errors, reason) - ) - - attributes = auth.get_attributes() - attributes['name_id'] = auth.get_nameid() - self._check_entitlements(idp, attributes) - response = { - 'idp_name': idp_name, - 'attributes': attributes, - 'session_index': auth.get_session_index(), - } - kwargs.update({'response': response, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - def _check_entitlements(self, idp, attributes): - """ - Additional verification of a SAML response before - authenticating the user. - - Subclasses can override this method if they need custom - validation code, such as requiring the presence of an - eduPersonEntitlement. - - raise social.exceptions.AuthForbidden if the user should not - be authenticated, or do nothing to allow the login pipeline to - continue. - """ - pass +from social_core.backends.saml import SAMLIdentityProvider, \ + DummySAMLIdentityProvider, SAMLAuth diff --git a/social/backends/shopify.py b/social/backends/shopify.py index a04b51284..22ee46dbb 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -1,92 +1 @@ -""" -Shopify OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/shopify.html -""" -import imp -import six - -from social.utils import handle_http_errors -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthFailed, AuthCanceled - - -class ShopifyOAuth2(BaseOAuth2): - """Shopify OAuth2 authentication backend""" - name = 'shopify' - ID_KEY = 'shop' - EXTRA_DATA = [ - ('shop', 'shop'), - ('website', 'website'), - ('expires', 'expires') - ] - REDIRECT_STATE = False - - @property - def shopifyAPI(self): - if not hasattr(self, '_shopify_api'): - fp, pathname, description = imp.find_module('shopify') - self._shopify_api = imp.load_module('shopify', fp, pathname, - description) - return self._shopify_api - - def get_user_details(self, response): - """Use the shopify store name as the username""" - return { - 'username': six.text_type(response.get('shop', '')).replace( - '.myshopify.com', '' - ) - } - - def extra_data(self, user, uid, response, details=None, *args, **kwargs): - """Return access_token and extra defined names to store in - extra_data field""" - data = super(ShopifyOAuth2, self).extra_data(user, uid, response, - details, *args, **kwargs) - session = self.shopifyAPI.Session(self.data.get('shop').strip()) - # Get, and store the permanent token - token = session.request_token(data['access_token']) - data['access_token'] = token - return dict(data) - - def auth_url(self): - key, secret = self.get_key_and_secret() - self.shopifyAPI.Session.setup(api_key=key, secret=secret) - scope = self.get_scope() - state = self.state_token() - self.strategy.session_set(self.name + '_state', state) - redirect_uri = self.get_redirect_uri(state) - session = self.shopifyAPI.Session(self.data.get('shop').strip()) - return session.create_permission_url( - scope=scope, - redirect_uri=redirect_uri - ) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - self.process_error(self.data) - access_token = None - key, secret = self.get_key_and_secret() - try: - shop_url = self.data.get('shop') - self.shopifyAPI.Session.setup(api_key=key, secret=secret) - shopify_session = self.shopifyAPI.Session(shop_url, self.data) - access_token = shopify_session.token - except self.shopifyAPI.ValidationException: - raise AuthCanceled(self) - else: - if not access_token: - raise AuthFailed(self, 'Authentication Failed') - return self.do_auth(access_token, shop_url, shopify_session.url, - *args, **kwargs) - - def do_auth(self, access_token, shop_url, website, *args, **kwargs): - kwargs.update({ - 'backend': self, - 'response': { - 'shop': shop_url, - 'website': 'http://{0}'.format(website), - 'access_token': access_token - } - }) - return self.strategy.authenticate(*args, **kwargs) +from social_core.backends.shopify import ShopifyOAuth2 diff --git a/social/backends/sketchfab.py b/social/backends/sketchfab.py index cb19ef5d4..243ff2667 100644 --- a/social/backends/sketchfab.py +++ b/social/backends/sketchfab.py @@ -1,39 +1 @@ -""" -Sketchfab OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/sketchfab.html - https://sketchfab.com/developers/oauth -""" -from social.backends.oauth import BaseOAuth2 - - -class SketchfabOAuth2(BaseOAuth2): - name = 'sketchfab' - ID_KEY = 'uid' - AUTHORIZATION_URL = 'https://sketchfab.com/oauth2/authorize/' - ACCESS_TOKEN_URL = 'https://sketchfab.com/oauth2/token/' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - REQUIRES_EMAIL_VALIDATION = False - EXTRA_DATA = [ - ('username', 'username'), - ('apiToken', 'apiToken') - ] - - def get_user_details(self, response): - """Return user details from Sketchfab account""" - user_data = response - email = user_data.get('email', '') - username = user_data['username'] - name = user_data.get('displayName', '') - fullname, first_name, last_name = self.get_user_names(name) - return {'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://sketchfab.com/v2/users/me', headers={ - 'Authorization': 'Bearer {0}'.format(access_token) - }) +from social_core.backends.sketchfab import SketchfabOAuth2 diff --git a/social/backends/skyrock.py b/social/backends/skyrock.py index b027d94ac..1b04e8c8f 100644 --- a/social/backends/skyrock.py +++ b/social/backends/skyrock.py @@ -1,32 +1 @@ -""" -Skyrock OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/skyrock.html -""" -from social.backends.oauth import BaseOAuth1 - - -class SkyrockOAuth(BaseOAuth1): - """Skyrock OAuth authentication backend""" - name = 'skyrock' - ID_KEY = 'id_user' - AUTHORIZATION_URL = 'https://api.skyrock.com/v2/oauth/authenticate' - REQUEST_TOKEN_URL = 'https://api.skyrock.com/v2/oauth/initiate' - ACCESS_TOKEN_URL = 'https://api.skyrock.com/v2/oauth/token' - EXTRA_DATA = [('id', 'id')] - - def get_user_details(self, response): - """Return user details from Skyrock account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response['firstname'], - last_name=response['name'] - ) - return {'username': response['username'], - 'email': response['email'], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token): - """Return user data provided""" - return self.get_json('https://api.skyrock.com/v2/user/get.json', - auth=self.oauth_auth(access_token)) +from social_core.backends.skyrock import SkyrockOAuth diff --git a/social/backends/slack.py b/social/backends/slack.py index ac6063487..874723151 100644 --- a/social/backends/slack.py +++ b/social/backends/slack.py @@ -1,66 +1 @@ -""" -Slack OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/slack.html - https://api.slack.com/docs/oauth -""" -import re - -from social.backends.oauth import BaseOAuth2 - - -class SlackOAuth2(BaseOAuth2): - """Slack OAuth authentication backend""" - name = 'slack' - AUTHORIZATION_URL = 'https://slack.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://slack.com/api/oauth.access' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('id', 'id'), - ('name', 'name'), - ('real_name', 'real_name') - ] - - def get_user_details(self, response): - """Return user details from Slack account""" - # Build the username with the team $username@$team_url - # Necessary to get unique names for all of slack - username = response.get('user') - if self.setting('USERNAME_WITH_TEAM', True): - match = re.search(r'//([^.]+)\.slack\.com', response['url']) - username = '{0}@{1}'.format(username, match.group(1)) - - out = {'username': username} - if 'profile' in response: - out.update({ - 'email': response['profile'].get('email'), - 'fullname': response['profile'].get('real_name'), - 'first_name': response['profile'].get('first_name'), - 'last_name': response['profile'].get('last_name') - }) - return out - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - # Has to be two calls, because the users.info requires a username, - # And we want the team information. Check auth.test details at: - # https://api.slack.com/methods/auth.test - auth_test = self.get_json('https://slack.com/api/auth.test', params={ - 'token': access_token - }) - - # https://api.slack.com/methods/users.info - user_info = self.get_json('https://slack.com/api/users.info', params={ - 'token': access_token, - 'user': auth_test.get('user_id') - }) - if user_info.get('user'): - # Capture the user data, if available based on the scope - auth_test.update(user_info['user']) - - # Clean up user_id vs id - auth_test['id'] = auth_test['user_id'] - auth_test.pop('ok', None) - auth_test.pop('user_id', None) - return auth_test +from social_core.backends.slack import SlackOAuth2 diff --git a/social/backends/soundcloud.py b/social/backends/soundcloud.py index ab41abd44..a8f1e9cf6 100644 --- a/social/backends/soundcloud.py +++ b/social/backends/soundcloud.py @@ -1,55 +1 @@ -""" -Soundcloud OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/soundcloud.html -""" -from social.p3 import urlencode -from social.backends.oauth import BaseOAuth2 - - -class SoundcloudOAuth2(BaseOAuth2): - """Soundcloud OAuth authentication backend""" - name = 'soundcloud' - AUTHORIZATION_URL = 'https://soundcloud.com/connect' - ACCESS_TOKEN_URL = 'https://api.soundcloud.com/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('id', 'id'), - ('refresh_token', 'refresh_token'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Soundcloud account""" - fullname, first_name, last_name = self.get_user_names( - response.get('full_name') - ) - return {'username': response.get('username'), - 'email': response.get('email') or '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://api.soundcloud.com/me.json', - params={'oauth_token': access_token}) - - def auth_url(self): - """Return redirect url""" - state = None - if self.STATE_PARAMETER or self.REDIRECT_STATE: - # Store state in session for further request validation. The state - # value is passed as state parameter (as specified in OAuth2 spec), - # but also added to redirect_uri, that way we can still verify the - # request if the provider doesn't implement the state parameter. - # Reuse token if any. - name = self.name + '_state' - state = self.strategy.session_get(name) or self.state_token() - self.strategy.session_set(name, state) - - params = self.auth_params(state) - params.update(self.get_scope_argument()) - params.update(self.auth_extra_arguments()) - return self.AUTHORIZATION_URL + '?' + urlencode(params) +from social_core.backends.soundcloud import SoundcloudOAuth2 diff --git a/social/backends/spotify.py b/social/backends/spotify.py index f2c64d7d8..d3e4e6a57 100644 --- a/social/backends/spotify.py +++ b/social/backends/spotify.py @@ -1,47 +1 @@ -""" -Spotify backend, docs at: - https://developer.spotify.com/spotify-web-api/ - https://developer.spotify.com/spotify-web-api/authorization-guide/ -""" -import base64 - -from social.backends.oauth import BaseOAuth2 - - -class SpotifyOAuth2(BaseOAuth2): - """Spotify OAuth2 authentication backend""" - name = 'spotify' - ID_KEY = 'id' - AUTHORIZATION_URL = 'https://accounts.spotify.com/authorize' - ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ' ' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('refresh_token', 'refresh_token'), - ] - - def auth_headers(self): - auth_str = '{0}:{1}'.format(*self.get_key_and_secret()) - b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode() - return { - 'Authorization': 'Basic {0}'.format(b64_auth_str) - } - - def get_user_details(self, response): - """Return user details from Spotify account""" - fullname, first_name, last_name = self.get_user_names( - response.get('display_name') - ) - return {'username': response.get('id'), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.spotify.com/v1/me', - headers={'Authorization': 'Bearer {0}'.format(access_token)} - ) +from social_core.backends.spotify import SpotifyOAuth2 diff --git a/social/backends/stackoverflow.py b/social/backends/stackoverflow.py index 8b6cbbfa8..957634e44 100644 --- a/social/backends/stackoverflow.py +++ b/social/backends/stackoverflow.py @@ -1,43 +1 @@ -""" -Stackoverflow OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/stackoverflow.html -""" -from social.backends.oauth import BaseOAuth2 - - -class StackoverflowOAuth2(BaseOAuth2): - """Stackoverflow OAuth2 authentication backend""" - name = 'stackoverflow' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'https://stackexchange.com/oauth' - ACCESS_TOKEN_URL = 'https://stackexchange.com/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Stackoverflow account""" - fullname, first_name, last_name = self.get_user_names( - response.get('display_name') - ) - return {'username': response.get('link').rsplit('/', 1)[-1], - 'full_name': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.stackexchange.com/2.1/me', - params={ - 'site': 'stackoverflow', - 'access_token': access_token, - 'key': self.setting('API_KEY') - } - )['items'][0] - - def request_access_token(self, *args, **kwargs): - return self.get_querystring(*args, **kwargs) +from social_core.backends.stackoverflow import StackoverflowOAuth2 diff --git a/social/backends/steam.py b/social/backends/steam.py index e283a5b63..6051ae247 100644 --- a/social/backends/steam.py +++ b/social/backends/steam.py @@ -1,47 +1 @@ -""" -Steam OpenId backend, docs at: - http://psa.matiasaguirre.net/docs/backends/steam.html -""" -from social.backends.open_id import OpenIdAuth -from social.exceptions import AuthFailed - - -USER_INFO = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?' - - -class SteamOpenId(OpenIdAuth): - name = 'steam' - URL = 'https://steamcommunity.com/openid' - - def get_user_id(self, details, response): - """Return user unique id provided by service""" - return self._user_id(response) - - def get_user_details(self, response): - player = self.get_json(USER_INFO, params={ - 'key': self.setting('API_KEY'), - 'steamids': self._user_id(response) - }) - if len(player['response']['players']) > 0: - player = player['response']['players'][0] - details = {'username': player.get('personaname'), - 'email': '', - 'fullname': '', - 'first_name': '', - 'last_name': '', - 'player': player} - else: - details = {} - return details - - def consumer(self): - # Steam seems to support stateless mode only, ignore store - if not hasattr(self, '_consumer'): - self._consumer = self.create_consumer() - return self._consumer - - def _user_id(self, response): - user_id = response.identity_url.rsplit('/', 1)[-1] - if not user_id.isdigit(): - raise AuthFailed(self, 'Missing Steam Id') - return user_id +from social_core.backends.steam import SteamOpenId diff --git a/social/backends/stocktwits.py b/social/backends/stocktwits.py index 55fee95ab..b7ff77196 100644 --- a/social/backends/stocktwits.py +++ b/social/backends/stocktwits.py @@ -1,37 +1 @@ -""" -Stocktwits OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/stocktwits.html -""" -from social.backends.oauth import BaseOAuth2 - - -class StocktwitsOAuth2(BaseOAuth2): - """Stockwiths OAuth2 backend""" - name = 'stocktwits' - AUTHORIZATION_URL = 'https://api.stocktwits.com/api/2/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api.stocktwits.com/api/2/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - DEFAULT_SCOPE = ['read', 'publish_messages', 'publish_watch_lists', - 'follow_users', 'follow_stocks'] - - def get_user_id(self, details, response): - return response['user']['id'] - - def get_user_details(self, response): - """Return user details from Stocktwits account""" - fullname, first_name, last_name = self.get_user_names( - response['user']['name'] - ) - return {'username': response['user']['username'], - 'email': '', # not supplied - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://api.stocktwits.com/api/2/account/verify.json', - params={'access_token': access_token} - ) +from social_core.backends.stocktwits import StocktwitsOAuth2 diff --git a/social/backends/strava.py b/social/backends/strava.py index 2eb6fcdf3..3e2f25fee 100644 --- a/social/backends/strava.py +++ b/social/backends/strava.py @@ -1,46 +1 @@ -""" -Strava OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/strava.html -""" -from social.backends.oauth import BaseOAuth2 - - -class StravaOAuth(BaseOAuth2): - name = 'strava' - AUTHORIZATION_URL = 'https://www.strava.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://www.strava.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - # Strava doesn't check for parameters in redirect_uri and directly appends - # the auth parameters to it, ending with an URL like: - # http://example.com/complete/strava?redirect_state=xxx?code=xxx&state=xxx - # Check issue #259 for details. - REDIRECT_STATE = False - REVOKE_TOKEN_URL = 'https://www.strava.com/oauth/deauthorize' - - def get_user_id(self, details, response): - return response['athlete']['id'] - - def get_user_details(self, response): - """Return user details from Strava account""" - # because there is no usernames on strava - username = response['athlete']['id'] - email = response['athlete'].get('email', '') - fullname, first_name, last_name = self.get_user_names( - first_name=response['athlete'].get('firstname', ''), - last_name=response['athlete'].get('lastname', ''), - ) - return {'username': str(username), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('https://www.strava.com/api/v3/athlete', - params={'access_token': access_token}) - - def revoke_token_params(self, token, uid): - params = super(StravaOAuth, self).revoke_token_params(token, uid) - params['access_token'] = token - return params +from social_core.backends.strava import StravaOAuth diff --git a/social/backends/stripe.py b/social/backends/stripe.py index 560c1390c..83927c052 100644 --- a/social/backends/stripe.py +++ b/social/backends/stripe.py @@ -1,54 +1 @@ -""" -Stripe OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/stripe.html -""" -from social.backends.oauth import BaseOAuth2 - - -class StripeOAuth2(BaseOAuth2): - """Stripe OAuth2 authentication backend""" - name = 'stripe' - ID_KEY = 'stripe_user_id' - AUTHORIZATION_URL = 'https://connect.stripe.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://connect.stripe.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('stripe_publishable_key', 'stripe_publishable_key'), - ('access_token', 'access_token'), - ('livemode', 'livemode'), - ('token_type', 'token_type'), - ('refresh_token', 'refresh_token'), - ('stripe_user_id', 'stripe_user_id'), - ] - - def get_user_details(self, response): - """Return user details from Stripe account""" - return {'username': response.get('stripe_user_id'), - 'email': ''} - - def auth_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - params = {'response_type': 'code', - 'client_id': client_id} - if state: - params['state'] = state - return params - - def auth_complete_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', - 'client_id': client_id, - 'scope': self.SCOPE_SEPARATOR.join(self.get_scope()), - 'code': self.data['code'] - } - - def auth_headers(self): - client_id, client_secret = self.get_key_and_secret() - return {'Accept': 'application/json', - 'Authorization': 'Bearer {0}'.format(client_secret)} - - def refresh_token_params(self, refresh_token, *args, **kwargs): - return {'refresh_token': refresh_token, - 'grant_type': 'refresh_token'} +from social_core.backends.stripe import StripeOAuth2 diff --git a/social/backends/suse.py b/social/backends/suse.py index 0e2d56b58..92ea7b558 100644 --- a/social/backends/suse.py +++ b/social/backends/suse.py @@ -1,17 +1 @@ -""" -Open Suse OpenId backend, docs at: - http://psa.matiasaguirre.net/docs/backends/suse.html -""" -from social.backends.open_id import OpenIdAuth - - -class OpenSUSEOpenId(OpenIdAuth): - name = 'opensuse' - URL = 'https://www.opensuse.org/openid/user/' - - def get_user_id(self, details, response): - """ - Return user unique id provided by service. For openSUSE - the nickname is original. - """ - return details['nickname'] +from social_core.backends.suse import OpenSUSEOpenId diff --git a/social/backends/taobao.py b/social/backends/taobao.py index 57839c97e..79a5c9595 100644 --- a/social/backends/taobao.py +++ b/social/backends/taobao.py @@ -1,26 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class TAOBAOAuth(BaseOAuth2): - """Taobao OAuth authentication mechanism""" - name = 'taobao' - ID_KEY = 'taobao_user_id' - ACCESS_TOKEN_METHOD = 'POST' - AUTHORIZATION_URL = 'https://oauth.taobao.com/authorize' - ACCESS_TOKEN_URL = 'https://oauth.taobao.com/token' - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - try: - return self.get_json('https://eco.taobao.com/router/rest', params={ - 'method': 'taobao.user.get', - 'fomate': 'json', - 'v': '2.0', - 'access_token': access_token - }) - except ValueError: - return None - - def get_user_details(self, response): - """Return user details from Taobao account""" - return {'username': response.get('taobao_user_nick')} +from social_core.backends.taobao import TAOBAOAuth diff --git a/social/backends/thisismyjam.py b/social/backends/thisismyjam.py index 994691f7d..dbc86cb25 100644 --- a/social/backends/thisismyjam.py +++ b/social/backends/thisismyjam.py @@ -1,33 +1 @@ -""" -ThisIsMyJam OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/thisismyjam.html -""" -from social.backends.oauth import BaseOAuth1 - - -class ThisIsMyJamOAuth1(BaseOAuth1): - """ThisIsMyJam OAuth1 authentication backend""" - name = 'thisismyjam' - REQUEST_TOKEN_URL = 'http://www.thisismyjam.com/oauth/request_token' - AUTHORIZATION_URL = 'http://www.thisismyjam.com/oauth/authorize' - ACCESS_TOKEN_URL = 'http://www.thisismyjam.com/oauth/access_token' - REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' - - def get_user_details(self, response): - """Return user details from ThisIsMyJam account""" - info = response.get('person') - fullname, first_name, last_name = self.get_user_names( - info.get('fullname') - ) - return { - 'username': info.get('name'), - 'email': '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json('http://api.thisismyjam.com/1/verify.json', - auth=self.oauth_auth(access_token)) +from social_core.backends.thisismyjam import ThisIsMyJamOAuth1 diff --git a/social/backends/trello.py b/social/backends/trello.py index f07cc4530..3b9c684c9 100644 --- a/social/backends/trello.py +++ b/social/backends/trello.py @@ -1,47 +1 @@ -""" -Trello OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/trello.html -""" -from social.backends.oauth import BaseOAuth1 - - -class TrelloOAuth(BaseOAuth1): - - """Trello OAuth authentication backend""" - name = 'trello' - ID_KEY = 'username' - AUTHORIZATION_URL = 'https://trello.com/1/OAuthAuthorizeToken' - REQUEST_TOKEN_URL = 'https://trello.com/1/OAuthGetRequestToken' - ACCESS_TOKEN_URL = 'https://trello.com/1/OAuthGetAccessToken' - - EXTRA_DATA = [ - ('username', 'username'), - ('email', 'email'), - ('fullName', 'fullName') - ] - - def get_user_details(self, response): - """Return user details from Trello account""" - fullname, first_name, last_name = self.get_user_names( - response.get('fullName') - ) - return {'username': response.get('username'), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token): - """Return user data provided""" - url = 'https://trello.com/1/members/me' - try: - return self.get_json(url, auth=self.oauth_auth(access_token)) - except ValueError: - return None - - def auth_extra_arguments(self): - return { - 'name': self.setting('APP_NAME', ''), - # trello default expiration is '30days' - 'expiration': self.setting('EXPIRATION', 'never') - } +from social_core.backends.trello import TrelloOAuth diff --git a/social/backends/tripit.py b/social/backends/tripit.py index ac09334be..209be29a4 100644 --- a/social/backends/tripit.py +++ b/social/backends/tripit.py @@ -1,43 +1 @@ -""" -Tripit OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/tripit.html -""" -from xml.dom import minidom - -from social.backends.oauth import BaseOAuth1 - - -class TripItOAuth(BaseOAuth1): - """TripIt OAuth authentication backend""" - name = 'tripit' - AUTHORIZATION_URL = 'https://www.tripit.com/oauth/authorize' - REQUEST_TOKEN_URL = 'https://api.tripit.com/oauth/request_token' - ACCESS_TOKEN_URL = 'https://api.tripit.com/oauth/access_token' - EXTRA_DATA = [('screen_name', 'screen_name')] - - def get_user_details(self, response): - """Return user details from TripIt account""" - fullname, first_name, last_name = self.get_user_names(response['name']) - return {'username': response['screen_name'], - 'email': response['email'], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - dom = minidom.parseString(self.oauth_request( - access_token, - 'https://api.tripit.com/v1/get/profile' - ).content) - return { - 'id': dom.getElementsByTagName('Profile')[0].getAttribute('ref'), - 'name': dom.getElementsByTagName('public_display_name')[0] - .childNodes[0].data, - 'screen_name': dom.getElementsByTagName('screen_name')[0] - .childNodes[0].data, - 'email': dom.getElementsByTagName('is_primary')[0] - .parentNode - .getElementsByTagName('address')[0] - .childNodes[0].data - } +from social_core.backends.tripit import TripItOAuth diff --git a/social/backends/tumblr.py b/social/backends/tumblr.py index 3d1eda2c7..33a71fbff 100644 --- a/social/backends/tumblr.py +++ b/social/backends/tumblr.py @@ -1,31 +1 @@ -""" -Tumblr OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/tumblr.html -""" -from social.utils import first -from social.backends.oauth import BaseOAuth1 - - -class TumblrOAuth(BaseOAuth1): - name = 'tumblr' - ID_KEY = 'name' - AUTHORIZATION_URL = 'http://www.tumblr.com/oauth/authorize' - REQUEST_TOKEN_URL = 'http://www.tumblr.com/oauth/request_token' - REQUEST_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_URL = 'http://www.tumblr.com/oauth/access_token' - - def get_user_id(self, details, response): - return response['response']['user'][self.ID_KEY] - - def get_user_details(self, response): - # http://www.tumblr.com/docs/en/api/v2#user-methods - user_info = response['response']['user'] - data = {'username': user_info['name']} - blog = first(lambda blog: blog['primary'], user_info['blogs']) - if blog: - data['fullname'] = blog['title'] - return data - - def user_data(self, access_token): - return self.get_json('http://api.tumblr.com/v2/user/info', - auth=self.oauth_auth(access_token)) +from social_core.backends.tumblr import TumblrOAuth diff --git a/social/backends/twilio.py b/social/backends/twilio.py index d63fe6c3a..39cbf5fa6 100644 --- a/social/backends/twilio.py +++ b/social/backends/twilio.py @@ -1,39 +1 @@ -""" -Twilio auth backend, docs at: - http://psa.matiasaguirre.net/docs/backends/twilio.html -""" -from re import sub - -from social.p3 import urlencode -from social.backends.base import BaseAuth - - -class TwilioAuth(BaseAuth): - name = 'twilio' - ID_KEY = 'AccountSid' - - def get_user_details(self, response): - """Return twilio details, Twilio only provides AccountSID as - parameters.""" - # /complete/twilio/?AccountSid=ACc65ea16c9ebd4d4684edf814995b27e - return {'username': response['AccountSid'], - 'email': '', - 'fullname': '', - 'first_name': '', - 'last_name': ''} - - def auth_url(self): - """Return authorization redirect url.""" - key, secret = self.get_key_and_secret() - callback = self.strategy.absolute_uri(self.redirect_uri) - callback = sub(r'^https', 'http', callback) - query = urlencode({'cb': callback}) - return 'https://www.twilio.com/authorize/{0}?{1}'.format(key, query) - - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - account_sid = self.data.get('AccountSid') - if not account_sid: - raise ValueError('No AccountSid returned') - kwargs.update({'response': self.data, 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) +from social_core.backends.twilio import TwilioAuth diff --git a/social/backends/twitch.py b/social/backends/twitch.py index efa995d56..35c3d88a9 100644 --- a/social/backends/twitch.py +++ b/social/backends/twitch.py @@ -1,30 +1 @@ -""" -Twitch OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/twitch.html -""" -from social.backends.oauth import BaseOAuth2 - - -class TwitchOAuth2(BaseOAuth2): - """Twitch OAuth authentication backend""" - name = 'twitch' - ID_KEY = '_id' - AUTHORIZATION_URL = 'https://api.twitch.tv/kraken/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://api.twitch.tv/kraken/oauth2/token' - ACCESS_TOKEN_METHOD = 'POST' - DEFAULT_SCOPE = ['user_read'] - REDIRECT_STATE = False - - def get_user_details(self, response): - return { - 'username': response.get('name'), - 'email': response.get('email'), - 'first_name': '', - 'last_name': '' - } - - def user_data(self, access_token, *args, **kwargs): - return self.get_json( - 'https://api.twitch.tv/kraken/user/', - params={'oauth_token': access_token} - ) +from social_core.backends.twitch import TwitchOAuth2 diff --git a/social/backends/twitter.py b/social/backends/twitter.py index 7ccb7d2cf..bcdb1682a 100644 --- a/social/backends/twitter.py +++ b/social/backends/twitter.py @@ -1,41 +1 @@ -""" -Twitter OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/twitter.html -""" -from social.backends.oauth import BaseOAuth1 -from social.exceptions import AuthCanceled - - -class TwitterOAuth(BaseOAuth1): - """Twitter OAuth authentication backend""" - name = 'twitter' - EXTRA_DATA = [('id', 'id')] - REQUEST_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_METHOD = 'POST' - AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authenticate' - REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' - ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' - REDIRECT_STATE = True - - def process_error(self, data): - if 'denied' in data: - raise AuthCanceled(self) - else: - super(TwitterOAuth, self).process_error(data) - - def get_user_details(self, response): - """Return user details from Twitter account""" - fullname, first_name, last_name = self.get_user_names(response['name']) - return {'username': response['screen_name'], - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json( - 'https://api.twitter.com/1.1/account/verify_credentials.json', - params={'include_email': 'true'}, - auth=self.oauth_auth(access_token) - ) +from social_core.backends.twitter import TwitterOAuth diff --git a/social/backends/uber.py b/social/backends/uber.py index 6b1463b94..c1b0c7530 100644 --- a/social/backends/uber.py +++ b/social/backends/uber.py @@ -1,39 +1 @@ -""" -Uber OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/uber.html -""" -from social.backends.oauth import BaseOAuth2 - - -class UberOAuth2(BaseOAuth2): - name = 'uber' - ID_KEY='uuid' - SCOPE_SEPARATOR = ' ' - AUTHORIZATION_URL = 'https://login.uber.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://login.uber.com/oauth/token' - ACCESS_TOKEN_METHOD = 'POST' - - def auth_complete_credentials(self): - return self.get_key_and_secret() - - def get_user_details(self, response): - """Return user details from Uber account""" - email = response.get('email', '') - fullname, first_name, last_name = self.get_user_names( - '', - response.get('first_name', ''), - response.get('last_name', '') - ) - return {'username': email, - 'email': email, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - response = kwargs.pop('response') - return self.get_json('https://api.uber.com/v1/me', headers={ - 'Authorization': '{0} {1}'.format(response.get('token_type'), - access_token) - }) +from social_core.backends.uber import UberOAuth2 diff --git a/social/backends/ubuntu.py b/social/backends/ubuntu.py index 64819c1de..c97e6b1f4 100644 --- a/social/backends/ubuntu.py +++ b/social/backends/ubuntu.py @@ -1,16 +1 @@ -""" -Ubuntu One OpenId backend -""" -from social.backends.open_id import OpenIdAuth - - -class UbuntuOpenId(OpenIdAuth): - name = 'ubuntu' - URL = 'https://login.ubuntu.com' - - def get_user_id(self, details, response): - """ - Return user unique id provided by service. For Ubuntu One - the nickname should be original. - """ - return details['nickname'] +from social_core.backends.ubuntu import UbuntuOpenId diff --git a/social/backends/untappd.py b/social/backends/untappd.py index 7d241adb5..33be4fca9 100644 --- a/social/backends/untappd.py +++ b/social/backends/untappd.py @@ -1,110 +1 @@ -import requests - -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthFailed -from social.utils import handle_http_errors - - -class UntappdOAuth2(BaseOAuth2): - """Untappd OAuth2 authentication backend""" - name = 'untappd' - AUTHORIZATION_URL = 'https://untappd.com/oauth/authenticate/' - ACCESS_TOKEN_URL = 'https://untappd.com/oauth/authorize/' - BASE_API_URL = 'https://api.untappd.com' - USER_INFO_URL = BASE_API_URL + '/v4/user/info/' - ACCESS_TOKEN_METHOD = 'GET' - STATE_PARAMETER = False - REDIRECT_STATE = False - EXTRA_DATA = [ - ('id', 'id'), - ('bio', 'bio'), - ('date_joined', 'date_joined'), - ('location', 'location'), - ('url', 'url'), - ('user_avatar', 'user_avatar'), - ('user_avatar_hd', 'user_avatar_hd'), - ('user_cover_photo', 'user_cover_photo') - ] - - def auth_params(self, state=None): - client_id, client_secret = self.get_key_and_secret() - params = { - 'client_id': client_id, - 'redirect_url': self.get_redirect_uri(), - 'response_type': self.RESPONSE_TYPE - } - return params - - def process_error(self, data): - """ - All errors from Untappd are contained in the 'meta' key of the response. - """ - response_code = data.get('meta', {}).get('http_code') - if response_code is not None and response_code != requests.codes.ok: - raise AuthFailed(self, data['meta']['error_detail']) - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance""" - client_id, client_secret = self.get_key_and_secret() - code = self.data.get('code') - - self.process_error(self.data) - - # Untapped sends the access token request with URL parameters, - # not a body - response = self.request_access_token( - self.access_token_url(), - method=self.ACCESS_TOKEN_METHOD, - params={ - 'response_type': 'code', - 'code': code, - 'client_id': client_id, - 'client_secret': client_secret, - 'redirect_url': self.get_redirect_uri() - } - ) - - self.process_error(response) - - # Both the access_token and the rest of the response are - # buried in the 'response' key - return self.do_auth( - response['response']['access_token'], - response=response['response'], - *args, **kwargs - ) - - def get_user_details(self, response): - """Return user details from an Untappd account""" - # Start with the user data as it was returned - user_data = response['user'] - - # Make a few updates to match expected key names - user_data.update({ - 'username': user_data.get('user_name'), - 'email': user_data.get('settings', {}).get('email_address', ''), - 'first_name': user_data.get('first_name'), - 'last_name': user_data.get('last_name'), - 'fullname': user_data.get('first_name') + ' ' + - user_data.get('last_name') - }) - return user_data - - def get_user_id(self, details, response): - """ - Return a unique ID for the current user, by default from - server response. - """ - return response['user'].get(self.ID_KEY) - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - response = self.get_json(self.USER_INFO_URL, params={ - 'access_token': access_token, - 'compact': 'true' - }) - self.process_error(response) - - # The response data is buried in the 'response' key - return response['response'] +from social_core.backends.untappd import UntappdOAuth2 diff --git a/social/backends/upwork.py b/social/backends/upwork.py index 64f4debc1..a648a672a 100644 --- a/social/backends/upwork.py +++ b/social/backends/upwork.py @@ -1,39 +1 @@ -""" -Upwork OAuth1 backend -""" -from social.backends.oauth import BaseOAuth1 - - -class UpworkOAuth(BaseOAuth1): - """Upwork OAuth authentication backend""" - name = 'upwork' - ID_KEY = 'id' - AUTHORIZATION_URL = 'https://www.upwork.com/services/api/auth' - REQUEST_TOKEN_URL = 'https://www.upwork.com/api/auth/v1/oauth/token/request' - REQUEST_TOKEN_METHOD = 'POST' - ACCESS_TOKEN_URL = 'https://www.upwork.com/api/auth/v1/oauth/token/access' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_URI_PARAMETER_NAME = 'oauth_callback' - - def get_user_details(self, response): - """Return user details from Upwork account""" - info = response.get('info', {}) - auth_user = response.get('auth_user', {}) - first_name = auth_user.get('first_name') - last_name = auth_user.get('last_name') - fullname = '{} {}'.format(first_name, last_name) - profile_url = info.get('profile_url', '') - username = profile_url.rsplit('/')[-1].replace('~', '') - return { - 'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - return self.get_json( - 'https://www.upwork.com/api/auth/v1/info.json', - auth=self.oauth_auth(access_token) - ) +from social_core.backends.upwork import UpworkOAuth diff --git a/social/backends/username.py b/social/backends/username.py index c88da09a5..36fcb6509 100644 --- a/social/backends/username.py +++ b/social/backends/username.py @@ -1,11 +1 @@ -""" -Legacy Username backend, docs at: - http://psa.matiasaguirre.net/docs/backends/username.html -""" -from social.backends.legacy import LegacyAuth - - -class UsernameAuth(LegacyAuth): - name = 'username' - ID_KEY = 'username' - EXTRA_DATA = ['username'] +from social_core.backends.username import UsernameAuth diff --git a/social/backends/utils.py b/social/backends/utils.py index 7650e31e6..f26dceaa1 100644 --- a/social/backends/utils.py +++ b/social/backends/utils.py @@ -1,80 +1,2 @@ -from social.exceptions import MissingBackend -from social.backends.base import BaseAuth -from social.utils import module_member, user_is_authenticated - - -# Cache for discovered backends. -BACKENDSCACHE = {} - - -def load_backends(backends, force_load=False): - """ - Load backends defined on SOCIAL_AUTH_AUTHENTICATION_BACKENDS, backends will - be imported and cached on BACKENDSCACHE. The key in that dict will be the - backend name, and the value is the backend class. - - Only subclasses of BaseAuth (and sub-classes) are considered backends. - - Previously there was a BACKENDS attribute expected on backends modules, - this is not needed anymore since it's enough with the - AUTHENTICATION_BACKENDS setting. BACKENDS was used because backends used to - be split on two classes the authentication backend and another class that - dealt with the auth mechanism with the provider, those classes are joined - now. - - A force_load boolean argument is also provided so that get_backend - below can retry a requested backend that may not yet be discovered. - """ - global BACKENDSCACHE - if force_load: - BACKENDSCACHE = {} - if not BACKENDSCACHE: - for auth_backend in backends: - backend = module_member(auth_backend) - if issubclass(backend, BaseAuth): - BACKENDSCACHE[backend.name] = backend - return BACKENDSCACHE - - -def get_backend(backends, name): - """Returns a backend by name. Backends are stored in the BACKENDSCACHE - cache dict. If not found, each of the modules referenced in - AUTHENTICATION_BACKENDS is imported and checked for a BACKENDS - definition. If the named backend is found in the module's BACKENDS - definition, it's then stored in the cache for future access. - """ - try: - # Cached backend which has previously been discovered - return BACKENDSCACHE[name] - except KeyError: - # Reload BACKENDS to ensure a missing backend hasn't been missed - load_backends(backends, force_load=True) - try: - return BACKENDSCACHE[name] - except KeyError: - raise MissingBackend(name) - - -def user_backends_data(user, backends, storage): - """ - Will return backends data for given user, the return value will have the - following keys: - associated: UserSocialAuth model instances for currently associated - accounts - not_associated: Not associated (yet) backend names - backends: All backend names. - - If user is not authenticated, then 'associated' list is empty, and there's - no difference between 'not_associated' and 'backends'. - """ - available = list(load_backends(backends).keys()) - values = {'associated': [], - 'not_associated': available, - 'backends': available} - if user_is_authenticated(user): - associated = storage.user.get_social_auth_for_user(user) - not_associated = list(set(available) - - set(assoc.provider for assoc in associated)) - values['associated'] = associated - values['not_associated'] = not_associated - return values +from social_core.backends.utils import load_backends, get_backend, \ + user_backends_data diff --git a/social/backends/vend.py b/social/backends/vend.py index 5c3927af4..addc3a6f8 100644 --- a/social/backends/vend.py +++ b/social/backends/vend.py @@ -1,39 +1 @@ -""" -Vend OAuth2 backend: -""" -from social.backends.oauth import BaseOAuth2 - - -class VendOAuth2(BaseOAuth2): - name = 'vend' - AUTHORIZATION_URL = 'https://secure.vendhq.com/connect' - ACCESS_TOKEN_URL = 'https://{0}.vendhq.com/api/1.0/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('refresh_token', 'refresh_token'), - ('domain_prefix', 'domain_prefix') - ] - - def access_token_url(self): - return self.ACCESS_TOKEN_URL.format(self.data['domain_prefix']) - - def get_user_details(self, response): - email = response['email'] - username = response.get('username') or email.split('@', 1)[0] - return { - 'username': username, - 'email': email, - 'fullname': '', - 'first_name': '', - 'last_name': '' - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - prefix = kwargs['response']['domain_prefix'] - url = 'https://{0}.vendhq.com/api/users'.format(prefix) - data = self.get_json(url, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) - }) - return data['users'][0] if data.get('users') else {} +from social_core.backends.vend import VendOAuth2 diff --git a/social/backends/vimeo.py b/social/backends/vimeo.py index 85d236f6a..45d7e0e66 100644 --- a/social/backends/vimeo.py +++ b/social/backends/vimeo.py @@ -1,79 +1 @@ -from social.backends.oauth import BaseOAuth1, BaseOAuth2 - - -class VimeoOAuth1(BaseOAuth1): - """Vimeo OAuth authentication backend""" - name = 'vimeo' - AUTHORIZATION_URL = 'https://vimeo.com/oauth/authorize' - REQUEST_TOKEN_URL = 'https://vimeo.com/oauth/request_token' - ACCESS_TOKEN_URL = 'https://vimeo.com/oauth/access_token' - - def get_user_id(self, details, response): - return response.get('person', {}).get('id') - - def get_user_details(self, response): - """Return user details from Twitter account""" - person = response.get('person', {}) - fullname, first_name, last_name = self.get_user_names( - person.get('display_name', '') - ) - return {'username': person.get('username', ''), - 'email': '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json( - 'https://vimeo.com/api/rest/v2', - params={'format': 'json', 'method': 'vimeo.people.getInfo'}, - auth=self.oauth_auth(access_token) - ) - - -class VimeoOAuth2(BaseOAuth2): - """Vimeo OAuth2 authentication backend""" - name = 'vimeo-oauth2' - AUTHORIZATION_URL = 'https://api.vimeo.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://api.vimeo.com/oauth/access_token' - REFRESH_TOKEN_URL = 'https://api.vimeo.com/oauth/request_token' - ACCESS_TOKEN_METHOD = 'POST' - SCOPE_SEPARATOR = ',' - API_ACCEPT_HEADER = {'Accept': 'application/vnd.vimeo.*+json;version=3.0'} - - def get_redirect_uri(self, state=None): - """ - Build redirect with redirect_state parameter. - - @Vimeo API 3 requires exact redirect uri without additional - additional state parameter included - """ - return self.redirect_uri - - def get_user_id(self, details, response): - """Return user id""" - try: - user_id = response.get('user', {})['uri'].split('/')[-1] - except KeyError: - user_id = None - return user_id - - def get_user_details(self, response): - """Return user details from account""" - user = response.get('user', {}) - fullname, first_name, last_name = self.get_user_names( - user.get('name', '') - ) - return {'username': fullname, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - return self.get_json( - 'https://api.vimeo.com/me', - params={'access_token': access_token}, - headers=VimeoOAuth2.API_ACCEPT_HEADER, - ) +from social_core.backends.vimeo import VimeoOAuth1, VimeoOAuth2 diff --git a/social/backends/vk.py b/social/backends/vk.py index db388d294..bef8107ff 100644 --- a/social/backends/vk.py +++ b/social/backends/vk.py @@ -1,208 +1,2 @@ -# -*- coding: utf-8 -*- -""" -VK.com OpenAPI, OAuth2 and Iframe application OAuth2 backends, docs at: - http://psa.matiasaguirre.net/docs/backends/vk.html -""" -from time import time -from hashlib import md5 - -from social.utils import parse_qs -from social.backends.base import BaseAuth -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthTokenRevoked, AuthException - - -class VKontakteOpenAPI(BaseAuth): - """VK.COM OpenAPI authentication backend""" - name = 'vk-openapi' - ID_KEY = 'id' - - def get_user_details(self, response): - """Return user details from VK.com request""" - nickname = response.get('nickname') or '' - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('first_name', [''])[0], - last_name=response.get('last_name', [''])[0] - ) - return { - 'username': response['id'] if len(nickname) == 0 else nickname, - 'email': '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - return self.data - - def auth_html(self): - """Returns local VK authentication page, not necessary for - VK to authenticate. - """ - ctx = {'VK_APP_ID': self.setting('APP_ID'), - 'VK_COMPLETE_URL': self.redirect_uri} - local_html = self.setting('LOCAL_HTML', 'vkontakte.html') - return self.strategy.render_html(tpl=local_html, context=ctx) - - def auth_complete(self, *args, **kwargs): - """Performs check of authentication in VKontakte, returns User if - succeeded""" - session_value = self.strategy.session_get( - 'vk_app_' + self.setting('APP_ID') - ) - if 'id' not in self.data or not session_value: - raise ValueError('VK.com authentication is not completed') - - mapping = parse_qs(session_value) - check_str = ''.join(item + '=' + mapping[item] - for item in ['expire', 'mid', 'secret', 'sid']) - - key, secret = self.get_key_and_secret() - hash = md5((check_str + secret).encode('utf-8')).hexdigest() - if hash != mapping['sig'] or int(mapping['expire']) < time(): - raise ValueError('VK.com authentication failed: Invalid Hash') - - kwargs.update({'backend': self, - 'response': self.user_data(mapping['mid'])}) - return self.strategy.authenticate(*args, **kwargs) - - def uses_redirect(self): - """VK.com does not require visiting server url in order - to do authentication, so auth_xxx methods are not needed to be called. - Their current implementation is just an example""" - return False - - -class VKOAuth2(BaseOAuth2): - """VKOAuth2 authentication backend""" - name = 'vk-oauth2' - ID_KEY = 'user_id' - AUTHORIZATION_URL = 'http://oauth.vk.com/authorize' - ACCESS_TOKEN_URL = 'https://oauth.vk.com/access_token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('id', 'id'), - ('expires_in', 'expires') - ] - - def get_user_details(self, response): - """Return user details from VK.com account""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('first_name'), - last_name=response.get('last_name') - ) - return {'username': response.get('screen_name'), - 'email': response.get('email', ''), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - request_data = ['first_name', 'last_name', 'screen_name', 'nickname', - 'photo'] + self.setting('EXTRA_DATA', []) - - fields = ','.join(set(request_data)) - data = vk_api(self, 'users.get', { - 'access_token': access_token, - 'fields': fields, - }) - - if data and data.get('error'): - error = data['error'] - msg = error.get('error_msg', 'Unknown error') - if error.get('error_code') == 5: - raise AuthTokenRevoked(self, msg) - else: - raise AuthException(self, msg) - - if data: - data = data.get('response')[0] - data['user_photo'] = data.get('photo') # Backward compatibility - return data or {} - - -class VKAppOAuth2(VKOAuth2): - """VK.com Application Authentication support""" - name = 'vk-app' - - def user_profile(self, user_id, access_token=None): - request_data = ['first_name', 'last_name', 'screen_name', 'nickname', - 'photo'] + self.setting('EXTRA_DATA', []) - fields = ','.join(set(request_data)) - data = {'uids': user_id, 'fields': fields} - if access_token: - data['access_token'] = access_token - profiles = vk_api(self, 'getProfiles', data).get('response') - if profiles: - return profiles[0] - - def auth_complete(self, *args, **kwargs): - required_params = ('is_app_user', 'viewer_id', 'access_token', - 'api_id') - if not all(param in self.data for param in required_params): - return None - - auth_key = self.data.get('auth_key') - - # Verify signature, if present - key, secret = self.get_key_and_secret() - if auth_key: - check_key = md5('_'.join([key, - self.data.get('viewer_id'), - secret]).encode('utf-8')).hexdigest() - if check_key != auth_key: - raise ValueError('VK.com authentication failed: invalid ' - 'auth key') - - user_check = self.setting('USERMODE') - user_id = self.data.get('viewer_id') - if user_check is not None: - user_check = int(user_check) - if user_check == 1: - is_user = self.data.get('is_app_user') - elif user_check == 2: - is_user = vk_api(self, 'isAppUser', - {'uid': user_id}).get('response', 0) - if not int(is_user): - return None - - auth_data = { - 'auth': self, - 'backend': self, - 'request': self.strategy.request_data(), - 'response': { - 'user_id': user_id, - } - } - auth_data['response'].update(self.user_profile(user_id)) - return self.strategy.authenticate(*args, **auth_data) - - -def vk_api(backend, method, data): - """ - Calls VK.com OpenAPI method, check: - https://vk.com/apiclub - http://goo.gl/yLcaa - """ - # We need to perform server-side call if no access_token - data['v'] = backend.setting('API_VERSION', '3.0') - if 'access_token' not in data: - key, secret = backend.get_key_and_secret() - if 'api_id' not in data: - data['api_id'] = key - - data['method'] = method - data['format'] = 'json' - url = 'http://api.vk.com/api.php' - param_list = sorted(list(item + '=' + data[item] for item in data)) - data['sig'] = md5( - (''.join(param_list) + secret).encode('utf-8') - ).hexdigest() - else: - url = 'https://api.vk.com/method/' + method - - try: - return backend.get_json(url, params=data) - except (TypeError, KeyError, IOError, ValueError, IndexError): - return None +from social_core.backends.vk import VKontakteOpenAPI, VKOAuth2, VKAppOAuth2, \ + vk_api diff --git a/social/backends/weibo.py b/social/backends/weibo.py index 6cc7844c3..e0f601141 100644 --- a/social/backends/weibo.py +++ b/social/backends/weibo.py @@ -1,61 +1 @@ -# coding:utf-8 -# author:hepochen@gmail.com https://github.com/hepochen -""" -Weibo OAuth2 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/weibo.html -""" -from social.backends.oauth import BaseOAuth2 - - -class WeiboOAuth2(BaseOAuth2): - """Weibo (of sina) OAuth authentication backend""" - name = 'weibo' - ID_KEY = 'uid' - AUTHORIZATION_URL = 'https://api.weibo.com/oauth2/authorize' - REQUEST_TOKEN_URL = 'https://api.weibo.com/oauth2/request_token' - ACCESS_TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('id', 'id'), - ('name', 'username'), - ('profile_image_url', 'profile_image_url'), - ('gender', 'gender') - ] - - def get_user_details(self, response): - """Return user details from Weibo. API URL is: - https://api.weibo.com/2/users/show.json/?uid=&access_token= - """ - if self.setting('DOMAIN_AS_USERNAME'): - username = response.get('domain', '') - else: - username = response.get('name', '') - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('screen_name', '') - ) - return {'username': username, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def get_uid(self, access_token): - """Return uid by access_token""" - data = self.get_json( - 'https://api.weibo.com/oauth2/get_token_info', - method='POST', - params={'access_token': access_token} - ) - return data['uid'] - - def user_data(self, access_token, response=None, *args, **kwargs): - """Return user data""" - # If user id was not retrieved in the response, then get it directly - # from weibo get_token_info endpoint - uid = response and response.get('uid') or self.get_uid(access_token) - user_data = self.get_json( - 'https://api.weibo.com/2/users/show.json', - params={'access_token': access_token, 'uid': uid} - ) - user_data['uid'] = uid - return user_data +from social_core.backends.weibo import WeiboOAuth2 diff --git a/social/backends/weixin.py b/social/backends/weixin.py index 279a03085..1ad748409 100644 --- a/social/backends/weixin.py +++ b/social/backends/weixin.py @@ -1,177 +1 @@ -# -*- coding: utf-8 -*- -# author:duoduo3369@gmail.com https://github.com/duoduo369 -""" -Weixin OAuth2 backend -""" -import urllib -from requests import HTTPError - -from social.backends.oauth import BaseOAuth2 -from social.exceptions import AuthCanceled, AuthUnknownError - - -class WeixinOAuth2(BaseOAuth2): - """Weixin OAuth authentication backend""" - name = 'weixin' - ID_KEY = 'openid' - AUTHORIZATION_URL = 'https://open.weixin.qq.com/connect/qrconnect' - ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('nickname', 'username'), - ('headimgurl', 'profile_image_url'), - ] - - def get_user_details(self, response): - """Return user details from Weixin. API URL is: - https://api.weixin.qq.com/sns/userinfo - """ - if self.setting('DOMAIN_AS_USERNAME'): - username = response.get('domain', '') - else: - username = response.get('nickname', '') - return { - 'username': username, - 'profile_image_url': response.get('headimgurl', '') - } - - def user_data(self, access_token, *args, **kwargs): - data = self.get_json('https://api.weixin.qq.com/sns/userinfo', params={ - 'access_token': access_token, - 'openid': kwargs['response']['openid'] - }) - nickname = data.get('nickname') - if nickname: - # weixin api has some encode bug, here need handle - data['nickname'] = nickname.encode( - 'raw_unicode_escape' - ).decode('utf-8') - return data - - def auth_params(self, state=None): - appid, secret = self.get_key_and_secret() - params = { - 'appid': appid, - 'redirect_uri': self.get_redirect_uri(state) - } - if self.STATE_PARAMETER and state: - params['state'] = state - if self.RESPONSE_TYPE: - params['response_type'] = self.RESPONSE_TYPE - return params - - def auth_complete_params(self, state=None): - appid, secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'appid': appid, - 'secret': secret, - 'redirect_uri': self.get_redirect_uri(state) - } - - def refresh_token_params(self, token, *args, **kwargs): - appid, secret = self.get_key_and_secret() - return { - 'refresh_token': token, - 'grant_type': 'refresh_token', - 'appid': appid, - 'secret': secret - } - - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self, response=err.response) - else: - raise - except KeyError: - raise AuthUnknownError(self) - if 'errcode' in response: - raise AuthCanceled(self) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) - - -class WeixinOAuth2APP(WeixinOAuth2): - """ - Weixin OAuth authentication backend - - Can't use in web, only in weixin app - """ - name = 'weixinapp' - ID_KEY = 'openid' - AUTHORIZATION_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize' - ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def auth_url(self): - if self.STATE_PARAMETER or self.REDIRECT_STATE: - # Store state in session for further request validation. The state - # value is passed as state parameter (as specified in OAuth2 spec), - # but also added to redirect, that way we can still verify the - # request if the provider doesn't implement the state parameter. - # Reuse token if any. - name = self.name + '_state' - state = self.strategy.session_get(name) - if state is None: - state = self.state_token() - self.strategy.session_set(name, state) - else: - state = None - - params = self.auth_params(state) - params.update(self.get_scope_argument()) - params.update(self.auth_extra_arguments()) - params = urllib.urlencode(sorted(params.items())) - return '{}#wechat_redirect'.format( - self.AUTHORIZATION_URL + '?' + params - ) - - def auth_complete_params(self, state=None): - appid, secret = self.get_key_and_secret() - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'appid': appid, - 'secret': secret, - } - - def validate_state(self): - return None - - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) - - if 'errcode' in response: - raise AuthCanceled(self) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) +from social_core.backends.weixin import WeixinOAuth2, WeixinOAuth2APP diff --git a/social/backends/withings.py b/social/backends/withings.py index dbe5d6d1c..7dc4dc113 100644 --- a/social/backends/withings.py +++ b/social/backends/withings.py @@ -1,14 +1 @@ -from social.backends.oauth import BaseOAuth1 - - -class WithingsOAuth(BaseOAuth1): - name = 'withings' - AUTHORIZATION_URL = 'https://oauth.withings.com/account/authorize' - REQUEST_TOKEN_URL = 'https://oauth.withings.com/account/request_token' - ACCESS_TOKEN_URL = 'https://oauth.withings.com/account/access_token' - ID_KEY = 'userid' - - def get_user_details(self, response): - """Return user details from Withings account""" - return {'userid': response['access_token']['userid'], - 'email': ''} +from social_core.backends.withings import WithingsOAuth diff --git a/social/backends/wunderlist.py b/social/backends/wunderlist.py index 9e3962831..7885f6e89 100644 --- a/social/backends/wunderlist.py +++ b/social/backends/wunderlist.py @@ -1,29 +1 @@ -from social.backends.oauth import BaseOAuth2 - - -class WunderlistOAuth2(BaseOAuth2): - """Wunderlist OAuth2 authentication backend""" - name = 'wunderlist' - AUTHORIZATION_URL = 'https://www.wunderlist.com/oauth/authorize' - ACCESS_TOKEN_URL = 'https://www.wunderlist.com/oauth/access_token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def get_user_details(self, response): - """Return user details from Wunderlist account""" - fullname, first_name, last_name = self.get_user_names( - response.get('name') - ) - return {'username': str(response.get('id')), - 'email': response.get('email'), - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - headers = { - 'X-Access-Token': access_token, - 'X-Client-ID': self.setting('KEY')} - return self.get_json( - 'https://a.wunderlist.com/api/v1/user', headers=headers) +from social_core.backends.wunderlist import WunderlistOAuth2 diff --git a/social/backends/xing.py b/social/backends/xing.py index 1a0381d4a..f0221b316 100644 --- a/social/backends/xing.py +++ b/social/backends/xing.py @@ -1,45 +1 @@ -""" -XING OAuth1 backend, docs at: - http://psa.matiasaguirre.net/docs/backends/xing.html -""" -from social.backends.oauth import BaseOAuth1 - - -class XingOAuth(BaseOAuth1): - """Xing OAuth authentication backend""" - name = 'xing' - AUTHORIZATION_URL = 'https://api.xing.com/v1/authorize' - REQUEST_TOKEN_URL = 'https://api.xing.com/v1/request_token' - ACCESS_TOKEN_URL = 'https://api.xing.com/v1/access_token' - SCOPE_SEPARATOR = '+' - EXTRA_DATA = [ - ('id', 'id'), - ('user_id', 'user_id') - ] - - def get_user_details(self, response): - """Return user details from Xing account""" - email = response.get('email', '') - fullname, first_name, last_name = self.get_user_names( - first_name=response['first_name'], - last_name=response['last_name'] - ) - return {'username': first_name + last_name, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'email': email} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - profile = self.get_json( - 'https://api.xing.com/v1/users/me.json', - auth=self.oauth_auth(access_token) - )['users'][0] - return { - 'user_id': profile['id'], - 'id': profile['id'], - 'first_name': profile['first_name'], - 'last_name': profile['last_name'], - 'email': profile['active_email'] - } +from social_core.backends.xing import XingOAuth diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 15fc9d17d..0c8ac84ae 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -1,159 +1 @@ -""" -Yahoo OpenId, OAuth1 and OAuth2 backends, docs at: - http://psa.matiasaguirre.net/docs/backends/yahoo.html -""" -from requests.auth import HTTPBasicAuth - -from social.utils import handle_http_errors -from social.backends.open_id import OpenIdAuth -from social.backends.oauth import BaseOAuth2, BaseOAuth1 - - -class YahooOpenId(OpenIdAuth): - """Yahoo OpenID authentication backend""" - name = 'yahoo' - URL = 'http://me.yahoo.com' - - -class YahooOAuth(BaseOAuth1): - """Yahoo OAuth authentication backend. DEPRECATED""" - name = 'yahoo-oauth' - ID_KEY = 'guid' - AUTHORIZATION_URL = 'https://api.login.yahoo.com/oauth/v2/request_auth' - REQUEST_TOKEN_URL = \ - 'https://api.login.yahoo.com/oauth/v2/get_request_token' - ACCESS_TOKEN_URL = 'https://api.login.yahoo.com/oauth/v2/get_token' - EXTRA_DATA = [ - ('guid', 'id'), - ('access_token', 'access_token'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Yahoo Profile""" - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('givenName'), - last_name=response.get('familyName') - ) - emails = [email for email in response.get('emails', []) - if email.get('handle')] - emails.sort(key=lambda e: e.get('primary', False), reverse=True) - return {'username': response.get('nickname'), - 'email': emails[0]['handle'] if emails else '', - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json' - return self.get_json( - url.format(self._get_guid(access_token)), - auth=self.oauth_auth(access_token) - )['profile'] - - def _get_guid(self, access_token): - """ - Beause you have to provide GUID for every API request it's also - returned during one of OAuth calls - """ - return self.get_json( - 'https://social.yahooapis.com/v1/me/guid?format=json', - auth=self.oauth_auth(access_token) - )['guid']['value'] - - -class YahooOAuth2(BaseOAuth2): - """Yahoo OAuth2 authentication backend""" - name = 'yahoo-oauth2' - ID_KEY = 'guid' - AUTHORIZATION_URL = 'https://api.login.yahoo.com/oauth2/request_auth' - ACCESS_TOKEN_URL = 'https://api.login.yahoo.com/oauth2/get_token' - ACCESS_TOKEN_METHOD = 'POST' - EXTRA_DATA = [ - ('xoauth_yahoo_guid', 'id'), - ('access_token', 'access_token'), - ('expires_in', 'expires'), - ('refresh_token', 'refresh_token'), - ('token_type', 'token_type'), - ] - - def get_user_names(self, first_name, last_name): - if first_name or last_name: - return ' '.join((first_name, last_name)), first_name, last_name - return None, None, None - - def get_user_details(self, response): - """ - Return user details from Yahoo Profile. - To Get user email you need the profile private read permission. - """ - fullname, first_name, last_name = self.get_user_names( - first_name=response.get('givenName'), - last_name=response.get('familyName') - ) - emails = [email for email in response.get('emails', []) - if 'handle' in email] - emails.sort(key=lambda e: e.get('primary', False), reverse=True) - email = emails[0]['handle'] if emails else response.get('guid', '') - return { - 'username': response.get('nickname'), - 'email': email, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name - } - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - url = 'https://social.yahooapis.com/v1/user/{0}/profile?format=json' \ - .format(kwargs['response']['xoauth_yahoo_guid']) - return self.get_json(url, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) - }, method='GET')['profile'] - - @handle_http_errors - def auth_complete(self, *args, **kwargs): - """Completes loging process, must return user instance""" - self.process_error(self.data) - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - auth=HTTPBasicAuth(*self.get_key_and_secret()), - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - self.process_error(response) - return self.do_auth(response['access_token'], response=response, - *args, **kwargs) - - def refresh_token_params(self, token, *args, **kwargs): - return { - 'refresh_token': token, - 'grant_type': 'refresh_token', - 'redirect_uri': 'oob', # out of bounds - } - - def refresh_token(self, token, *args, **kwargs): - params = self.refresh_token_params(token, *args, **kwargs) - url = self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL - method = self.REFRESH_TOKEN_METHOD - key = 'params' if method == 'GET' else 'data' - request_args = { - 'headers': self.auth_headers(), - 'method': method, - key: params - } - request = self.request( - url, - auth=HTTPBasicAuth(*self.get_key_and_secret()), - **request_args - ) - return self.process_refresh_token_response(request, *args, **kwargs) - - def auth_complete_params(self, state=None): - return { - 'grant_type': 'authorization_code', # request auth code - 'code': self.data.get('code', ''), # server response code - 'redirect_uri': self.get_redirect_uri(state) - } +from social_core.backends.yahoo import YahooOpenId, YahooOAuth, YahooOAuth2 diff --git a/social/backends/yammer.py b/social/backends/yammer.py index f52e28cf0..2fb1c08b2 100644 --- a/social/backends/yammer.py +++ b/social/backends/yammer.py @@ -1,44 +1 @@ -""" -Yammer OAuth2 production and staging backends, docs at: - http://psa.matiasaguirre.net/docs/backends/yammer.html -""" -from social.backends.oauth import BaseOAuth2 - - -class YammerOAuth2(BaseOAuth2): - name = 'yammer' - AUTHORIZATION_URL = 'https://www.yammer.com/dialog/oauth' - ACCESS_TOKEN_URL = 'https://www.yammer.com/oauth2/access_token' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires'), - ('mugshot_url', 'mugshot_url') - ] - - def get_user_id(self, details, response): - return response['user']['id'] - - def get_user_details(self, response): - username = response['user']['name'] - fullname, first_name, last_name = self.get_user_names( - fullname=response['user']['full_name'], - first_name=response['user']['first_name'], - last_name=response['user']['last_name'] - ) - email = response['user']['contact']['email_addresses'][0]['address'] - mugshot_url = response['user']['mugshot_url'] - return { - 'username': username, - 'email': email, - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name, - 'picture_url': mugshot_url - } - - -class YammerStagingOAuth2(YammerOAuth2): - name = 'yammer-staging' - AUTHORIZATION_URL = 'https://www.staging.yammer.com/dialog/oauth' - ACCESS_TOKEN_URL = 'https://www.staging.yammer.com/oauth2/access_token' - REQUEST_TOKEN_URL = 'https://www.staging.yammer.com/oauth2/request_token' +from social_core.backends.yammer import YammerOAuth2, YammerStagingOAuth2 diff --git a/social/backends/yandex.py b/social/backends/yandex.py index ab53366f2..14afd7777 100644 --- a/social/backends/yandex.py +++ b/social/backends/yandex.py @@ -1,78 +1 @@ -""" -Yandex OpenID and OAuth2 support. - -This contribution adds support for Yandex.ru OpenID service in the form -openid.yandex.ru/user. Username is retrieved from the identity url. - -If username is not specified, OpenID 2.0 url used for authentication. -""" -from social.p3 import urlsplit -from social.backends.open_id import OpenIdAuth -from social.backends.oauth import BaseOAuth2 - - -class YandexOpenId(OpenIdAuth): - """Yandex OpenID authentication backend""" - name = 'yandex-openid' - URL = 'http://openid.yandex.ru' - - def get_user_id(self, details, response): - return details['email'] or response.identity_url - - def get_user_details(self, response): - """Generate username from identity url""" - values = super(YandexOpenId, self).get_user_details(response) - values['username'] = values.get('username') or\ - urlsplit(response.identity_url)\ - .path.strip('/') - values['email'] = values.get('email', '') - return values - - -class YandexOAuth2(BaseOAuth2): - """Legacy Yandex OAuth2 authentication backend""" - name = 'yandex-oauth2' - AUTHORIZATION_URL = 'https://oauth.yandex.com/authorize' - ACCESS_TOKEN_URL = 'https://oauth.yandex.com/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - response.get('real_name') or response.get('display_name') or '' - ) - return {'username': response.get('display_name'), - 'email': response.get('default_email') or - response.get('emails', [''])[0], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://login.yandex.ru/info', - params={'oauth_token': access_token, - 'format': 'json'}) - - -class YaruOAuth2(BaseOAuth2): - name = 'yaru' - AUTHORIZATION_URL = 'https://oauth.yandex.com/authorize' - ACCESS_TOKEN_URL = 'https://oauth.yandex.com/token' - ACCESS_TOKEN_METHOD = 'POST' - REDIRECT_STATE = False - - def get_user_details(self, response): - fullname, first_name, last_name = self.get_user_names( - response.get('real_name') or response.get('display_name') or '' - ) - return {'username': response.get('display_name'), - 'email': response.get('default_email') or - response.get('emails', [''])[0], - 'fullname': fullname, - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - return self.get_json('https://login.yandex.ru/info', - params={'oauth_token': access_token, - 'format': 'json'}) +from social_core.backends.yandex import YandexOpenId, YandexOAuth2, YaruOAuth2 diff --git a/social/backends/zotero.py b/social/backends/zotero.py index 3e544846b..ee0feffcf 100644 --- a/social/backends/zotero.py +++ b/social/backends/zotero.py @@ -1,29 +1 @@ -""" -Zotero OAuth1 backends, docs at: - http://psa.matiasaguirre.net/docs/backends/zotero.html -""" -from social.backends.oauth import BaseOAuth1 - - -class ZoteroOAuth(BaseOAuth1): - - """Zotero OAuth authorization mechanism""" - name = 'zotero' - AUTHORIZATION_URL = 'https://www.zotero.org/oauth/authorize' - REQUEST_TOKEN_URL = 'https://www.zotero.org/oauth/request' - ACCESS_TOKEN_URL = 'https://www.zotero.org/oauth/access' - - def get_user_id(self, details, response): - """ - Return user unique id provided by service. For Ubuntu One - the nickname should be original. - """ - return details['userID'] - - def get_user_details(self, response): - """Return user details from Zotero API account""" - access_token = response.get('access_token', {}) - return { - 'username': access_token.get('username', ''), - 'userID': access_token.get('userID', '') - } +from social_core.backends.zotero import ZoteroOAuth diff --git a/social/exceptions.py b/social/exceptions.py index aa174c970..8d1b2eff2 100644 --- a/social/exceptions.py +++ b/social/exceptions.py @@ -1,113 +1,5 @@ -class SocialAuthBaseException(ValueError): - """Base class for pipeline exceptions.""" - pass - - -class WrongBackend(SocialAuthBaseException): - def __init__(self, backend_name): - self.backend_name = backend_name - - def __str__(self): - return 'Incorrect authentication service "{0}"'.format( - self.backend_name - ) - - -class MissingBackend(WrongBackend): - def __str__(self): - return 'Missing backend "{0}" entry'.format(self.backend_name) - - -class NotAllowedToDisconnect(SocialAuthBaseException): - """User is not allowed to disconnect it's social account.""" - pass - - -class AuthException(SocialAuthBaseException): - """Auth process exception.""" - def __init__(self, backend, *args, **kwargs): - self.backend = backend - super(AuthException, self).__init__(*args, **kwargs) - - -class AuthFailed(AuthException): - """Auth process failed for some reason.""" - def __str__(self): - msg = super(AuthFailed, self).__str__() - if msg == 'access_denied': - return 'Authentication process was canceled' - return 'Authentication failed: {0}'.format(msg) - - -class AuthCanceled(AuthException): - """Auth process was canceled by user.""" - def __init__(self, *args, **kwargs): - self.response = kwargs.pop('response', None) - super(AuthCanceled, self).__init__(*args, **kwargs) - - def __str__(self): - return 'Authentication process canceled' - - -class AuthUnknownError(AuthException): - """Unknown auth process error.""" - def __str__(self): - msg = super(AuthUnknownError, self).__str__() - return 'An unknown error happened while authenticating {0}'.format(msg) - - -class AuthTokenError(AuthException): - """Auth token error.""" - def __str__(self): - msg = super(AuthTokenError, self).__str__() - return 'Token error: {0}'.format(msg) - - -class AuthMissingParameter(AuthException): - """Missing parameter needed to start or complete the process.""" - def __init__(self, backend, parameter, *args, **kwargs): - self.parameter = parameter - super(AuthMissingParameter, self).__init__(backend, *args, **kwargs) - - def __str__(self): - return 'Missing needed parameter {0}'.format(self.parameter) - - -class AuthStateMissing(AuthException): - """State parameter is incorrect.""" - def __str__(self): - return 'Session value state missing.' - - -class AuthStateForbidden(AuthException): - """State parameter is incorrect.""" - def __str__(self): - return 'Wrong state parameter given.' - - -class AuthAlreadyAssociated(AuthException): - """A different user has already associated the target social account""" - pass - - -class AuthTokenRevoked(AuthException): - """User revoked the access_token in the provider.""" - def __str__(self): - return 'User revoke access to the token' - - -class AuthForbidden(AuthException): - """Authentication for this user is forbidden""" - def __str__(self): - return 'Your credentials aren\'t allowed' - - -class AuthUnreachableProvider(AuthException): - """Cannot reach the provider""" - def __str__(self): - return 'The authentication provider could not be reached' - - -class InvalidEmail(AuthException): - def __str__(self): - return 'Email couldn\'t be validated' +from social_core.exceptions import SocialAuthBaseException, WrongBackend, \ + MissingBackend, NotAllowedToDisconnect, AuthException, AuthFailed, \ + AuthCanceled, AuthUnknownError, AuthTokenError, AuthMissingParameter, \ + AuthStateMissing, AuthStateForbidden, AuthAlreadyAssociated, \ + AuthTokenRevoked, AuthForbidden, AuthUnreachableProvider, InvalidEmail diff --git a/social/pipeline/__init__.py b/social/pipeline/__init__.py index f87242d5a..44dbcfe15 100644 --- a/social/pipeline/__init__.py +++ b/social/pipeline/__init__.py @@ -1,59 +1,2 @@ -DEFAULT_AUTH_PIPELINE = ( - # Get the information we can about the user and return it in a simple - # format to create the user instance later. On some cases the details are - # already part of the auth response from the provider, but sometimes this - # could hit a provider API. - 'social.pipeline.social_auth.social_details', - - # Get the social uid from whichever service we're authing thru. The uid is - # the unique identifier of the given user in the provider. - 'social.pipeline.social_auth.social_uid', - - # Verifies that the current auth process is valid within the current - # project, this is where emails and domains whitelists are applied (if - # defined). - 'social.pipeline.social_auth.auth_allowed', - - # Checks if the current social-account is already associated in the site. - 'social.pipeline.social_auth.social_user', - - # Make up a username for this person, appends a random string at the end if - # there's any collision. - 'social.pipeline.user.get_username', - - # Send a validation email to the user to verify its email address. - # 'social.pipeline.mail.mail_validation', - - # Associates the current social details with another user account with - # a similar email address. - # 'social.pipeline.social_auth.associate_by_email', - - # Create a user account if we haven't found one yet. - 'social.pipeline.user.create_user', - - # Create the record that associated the social account with this user. - 'social.pipeline.social_auth.associate_user', - - # Populate the extra_data field in the social record with the values - # specified by settings (and the default ones like access_token, etc). - 'social.pipeline.social_auth.load_extra_data', - - # Update the user record with any changed info from the auth service. - 'social.pipeline.user.user_details' -) - -DEFAULT_DISCONNECT_PIPELINE = ( - # Verifies that the social association can be disconnected from the current - # user (ensure that the user login mechanism is not compromised by this - # disconnection). - 'social.pipeline.disconnect.allowed_to_disconnect', - - # Collects the social associations to disconnect. - 'social.pipeline.disconnect.get_entries', - - # Revoke any access_token when possible. - 'social.pipeline.disconnect.revoke_tokens', - - # Removes the social associations. - 'social.pipeline.disconnect.disconnect' -) +from social_core.pipeline import DEFAULT_AUTH_PIPELINE, \ + DEFAULT_DISCONNECT_PIPELINE diff --git a/social/pipeline/debug.py b/social/pipeline/debug.py index 7ef102269..c3dd6e92e 100644 --- a/social/pipeline/debug.py +++ b/social/pipeline/debug.py @@ -1,13 +1 @@ -from pprint import pprint - - -def debug(response, details, *args, **kwargs): - print('=' * 80) - pprint(response) - print('=' * 80) - pprint(details) - print('=' * 80) - pprint(args) - print('=' * 80) - pprint(kwargs) - print('=' * 80) +from social_core.pipeline.debug import debug diff --git a/social/pipeline/disconnect.py b/social/pipeline/disconnect.py index 9dd8ff4de..c2a9ac50d 100644 --- a/social/pipeline/disconnect.py +++ b/social/pipeline/disconnect.py @@ -1,31 +1,2 @@ -from social.exceptions import NotAllowedToDisconnect - - -def allowed_to_disconnect(strategy, user, name, user_storage, - association_id=None, *args, **kwargs): - if not user_storage.allowed_to_disconnect(user, name, association_id): - raise NotAllowedToDisconnect() - - -def get_entries(strategy, user, name, user_storage, association_id=None, - *args, **kwargs): - return { - 'entries': user_storage.get_social_auth_for_user( - user, name, association_id - ) - } - - -def revoke_tokens(strategy, entries, *args, **kwargs): - revoke_tokens = strategy.setting('REVOKE_TOKENS_ON_DISCONNECT', False) - if revoke_tokens: - for entry in entries: - if 'access_token' in entry.extra_data: - backend = entry.get_backend(strategy)(strategy) - backend.revoke_token(entry.extra_data['access_token'], - entry.uid) - - -def disconnect(strategy, entries, user_storage, *args, **kwargs): - for entry in entries: - user_storage.disconnect(entry) +from social_core.pipeline.disconnect import allowed_to_disconnect, \ + get_entries, revoke_tokens, disconnect diff --git a/social/pipeline/mail.py b/social/pipeline/mail.py index c52933098..f099d5b40 100644 --- a/social/pipeline/mail.py +++ b/social/pipeline/mail.py @@ -1,24 +1 @@ -from social.exceptions import InvalidEmail -from social.pipeline.partial import partial - - -@partial -def mail_validation(backend, details, is_new=False, *args, **kwargs): - requires_validation = backend.REQUIRES_EMAIL_VALIDATION or \ - backend.setting('FORCE_EMAIL_VALIDATION', False) - send_validation = details.get('email') and \ - (is_new or backend.setting('PASSWORDLESS', False)) - if requires_validation and send_validation: - data = backend.strategy.request_data() - if 'verification_code' in data: - backend.strategy.session_pop('email_validation_address') - if not backend.strategy.validate_email(details['email'], - data['verification_code']): - raise InvalidEmail(backend) - else: - backend.strategy.send_email_validation(backend, details['email']) - backend.strategy.session_set('email_validation_address', - details['email']) - return backend.strategy.redirect( - backend.strategy.setting('EMAIL_VALIDATION_URL') - ) +from social_core.pipeline.mail import mail_validation diff --git a/social/pipeline/partial.py b/social/pipeline/partial.py index 51a13aa74..e7a9ab822 100644 --- a/social/pipeline/partial.py +++ b/social/pipeline/partial.py @@ -1,21 +1 @@ -from functools import wraps - - -def save_status_to_session(strategy, pipeline_index, *args, **kwargs): - """Saves current social-auth status to session.""" - strategy.session_set('partial_pipeline', - strategy.partial_to_session(pipeline_index + 1, - *args, **kwargs)) - - -def partial(func): - @wraps(func) - def wrapper(strategy, pipeline_index, *args, **kwargs): - out = func(strategy=strategy, pipeline_index=pipeline_index, - *args, **kwargs) or {} - if not isinstance(out, dict): - values = strategy.partial_to_session(pipeline_index, *args, - **kwargs) - strategy.session_set('partial_pipeline', values) - return out - return wrapper +from social_core.pipeline.partial import save_status_to_session, partial diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py index b181ba556..1b56bb696 100644 --- a/social/pipeline/social_auth.py +++ b/social/pipeline/social_auth.py @@ -1,89 +1,3 @@ -from social.exceptions import AuthAlreadyAssociated, AuthException, \ - AuthForbidden - - -def social_details(backend, details, response, *args, **kwargs): - return {'details': dict(backend.get_user_details(response), **details)} - - -def social_uid(backend, details, response, *args, **kwargs): - return {'uid': backend.get_user_id(details, response)} - - -def auth_allowed(backend, details, response, *args, **kwargs): - if not backend.auth_allowed(response, details): - raise AuthForbidden(backend) - - -def social_user(backend, uid, user=None, *args, **kwargs): - provider = backend.name - social = backend.strategy.storage.user.get_social_auth(provider, uid) - if social: - if user and social.user != user: - msg = 'This {0} account is already in use.'.format(provider) - raise AuthAlreadyAssociated(backend, msg) - elif not user: - user = social.user - return {'social': social, - 'user': user, - 'is_new': user is None, - 'new_association': social is None} - - -def associate_user(backend, uid, user=None, social=None, *args, **kwargs): - if user and not social: - try: - social = backend.strategy.storage.user.create_social_auth( - user, uid, backend.name - ) - except Exception as err: - if not backend.strategy.storage.is_integrity_error(err): - raise - # Protect for possible race condition, those bastard with FTL - # clicking capabilities, check issue #131: - # https://github.com/omab/django-social-auth/issues/131 - return social_user(backend, uid, user, *args, **kwargs) - else: - return {'social': social, - 'user': social.user, - 'new_association': True} - - -def associate_by_email(backend, details, user=None, *args, **kwargs): - """ - Associate current auth with a user with the same email address in the DB. - - This pipeline entry is not 100% secure unless you know that the providers - enabled enforce email verification on their side, otherwise a user can - attempt to take over another user account by using the same (not validated) - email address on some provider. This pipeline entry is disabled by - default. - """ - if user: - return None - - email = details.get('email') - if email: - # Try to associate accounts registered with the same email address, - # only if it's a single object. AuthException is raised if multiple - # objects are returned. - users = list(backend.strategy.storage.user.get_users_by_email(email)) - if len(users) == 0: - return None - elif len(users) > 1: - raise AuthException( - backend, - 'The given email address is associated with another account' - ) - else: - return {'user': users[0], - 'is_new': False} - - -def load_extra_data(backend, details, response, uid, user, *args, **kwargs): - social = kwargs.get('social') or \ - backend.strategy.storage.user.get_social_auth(backend.name, uid) - if social: - extra_data = backend.extra_data(user, uid, response, details, - *args, **kwargs) - social.set_extra_data(extra_data) +from social_core.pipeline.social_auth import social_details, social_uid, \ + auth_allowed, social_user, associate_user, associate_by_email, \ + load_extra_data diff --git a/social/pipeline/user.py b/social/pipeline/user.py index e5f8d65fd..31befea17 100644 --- a/social/pipeline/user.py +++ b/social/pipeline/user.py @@ -1,95 +1,2 @@ -from uuid import uuid4 - -from social.utils import slugify, module_member - - -USER_FIELDS = ['username', 'email'] - - -def get_username(strategy, details, user=None, *args, **kwargs): - if 'username' not in strategy.setting('USER_FIELDS', USER_FIELDS): - return - storage = strategy.storage - - if not user: - email_as_username = strategy.setting('USERNAME_IS_FULL_EMAIL', False) - uuid_length = strategy.setting('UUID_LENGTH', 16) - max_length = storage.user.username_max_length() - do_slugify = strategy.setting('SLUGIFY_USERNAMES', False) - do_clean = strategy.setting('CLEAN_USERNAMES', True) - - if do_clean: - clean_func = storage.user.clean_username - else: - clean_func = lambda val: val - - if do_slugify: - override_slug = strategy.setting('SLUGIFY_FUNCTION') - if override_slug: - slug_func = module_member(override_slug) - else: - slug_func = slugify - else: - slug_func = lambda val: val - - if email_as_username and details.get('email'): - username = details['email'] - elif details.get('username'): - username = details['username'] - else: - username = uuid4().hex - - short_username = (username[:max_length - uuid_length] - if max_length is not None - else username) - final_username = slug_func(clean_func(username[:max_length])) - - # Generate a unique username for current user using username - # as base but adding a unique hash at the end. Original - # username is cut to avoid any field max_length. - # The final_username may be empty and will skip the loop. - while not final_username or \ - storage.user.user_exists(username=final_username): - username = short_username + uuid4().hex[:uuid_length] - final_username = slug_func(clean_func(username[:max_length])) - else: - final_username = storage.user.get_username(user) - return {'username': final_username} - - -def create_user(strategy, details, user=None, *args, **kwargs): - if user: - return {'is_new': False} - - fields = dict((name, kwargs.get(name, details.get(name))) - for name in strategy.setting('USER_FIELDS', USER_FIELDS)) - if not fields: - return - - return { - 'is_new': True, - 'user': strategy.create_user(**fields) - } - - -def user_details(strategy, details, user=None, *args, **kwargs): - """Update user details using data from provider.""" - if user: - changed = False # flag to track changes - protected = ('username', 'id', 'pk', 'email') + \ - tuple(strategy.setting('PROTECTED_USER_FIELDS', [])) - - # Update user model attributes with the new data sent by the current - # provider. Update on some attributes is disabled by default, for - # example username and id fields. It's also possible to disable update - # on fields defined in SOCIAL_AUTH_PROTECTED_FIELDS. - for name, value in details.items(): - if value and hasattr(user, name): - # Check https://github.com/omab/python-social-auth/issues/671 - current_value = getattr(user, name, None) - if not current_value or name not in protected: - changed |= current_value != value - setattr(user, name, value) - - if changed: - strategy.storage.user.changed(user) +from social_core.pipeline.user import USER_FIELDS, get_username, create_user, \ + user_details diff --git a/social/pipeline/utils.py b/social/pipeline/utils.py index b1713090a..2497d541e 100644 --- a/social/pipeline/utils.py +++ b/social/pipeline/utils.py @@ -1,61 +1,2 @@ -import six - - -SERIALIZABLE_TYPES = (dict, list, tuple, set, bool, type(None)) + \ - six.integer_types + six.string_types + \ - (six.text_type, six.binary_type,) - - -def partial_to_session(strategy, next, backend, request=None, *args, **kwargs): - user = kwargs.get('user') - social = kwargs.get('social') - clean_kwargs = { - 'response': kwargs.get('response') or {}, - 'details': kwargs.get('details') or {}, - 'username': kwargs.get('username'), - 'uid': kwargs.get('uid'), - 'is_new': kwargs.get('is_new') or False, - 'new_association': kwargs.get('new_association') or False, - 'user': user and user.id or None, - 'social': social and { - 'provider': social.provider, - 'uid': social.uid - } or None - } - - kwargs.update(clean_kwargs) - - # Clean any MergeDict data type from the values - new_kwargs = {} - for name, value in kwargs.items(): - # Check for class name to avoid importing Django MergeDict or - # Werkzeug MultiDict - if isinstance(value, dict) or \ - value.__class__.__name__ in ('MergeDict', 'MultiDict'): - value = dict(value) - if isinstance(value, SERIALIZABLE_TYPES): - new_kwargs[name] = strategy.to_session_value(value) - - return { - 'next': next, - 'backend': backend.name, - 'args': tuple(map(strategy.to_session_value, args)), - 'kwargs': new_kwargs - } - - -def partial_from_session(strategy, session): - kwargs = session['kwargs'].copy() - user = kwargs.get('user') - social = kwargs.get('social') - if isinstance(social, dict): - kwargs['social'] = strategy.storage.user.get_social_auth(**social) - if user: - kwargs['user'] = strategy.storage.user.get_user(user) - return ( - session['next'], - session['backend'], - list(map(strategy.from_session_value, session['args'])), - dict((key, strategy.from_session_value(val)) - for key, val in kwargs.items()) - ) +from social_core.pipeline.utils import SERIALIZABLE_TYPES, partial_to_session, \ + partial_from_session diff --git a/social/storage/base.py b/social/storage/base.py index 5df42e372..4d549a571 100644 --- a/social/storage/base.py +++ b/social/storage/base.py @@ -1,257 +1,2 @@ -"""Models mixins for Social Auth""" -import re -import time -import base64 -import uuid -import warnings - -from datetime import datetime, timedelta - -import six - -from openid.association import Association as OpenIdAssociation - -from social.backends.utils import get_backend -from social.strategies.utils import get_current_strategy - - -CLEAN_USERNAME_REGEX = re.compile(r'[^\w.@+_-]+', re.UNICODE) - - -class UserMixin(object): - user = '' - provider = '' - uid = None - extra_data = None - - def get_backend(self, strategy=None): - strategy = strategy or get_current_strategy() - if strategy: - return get_backend(strategy.get_backends(), self.provider) - - def get_backend_instance(self, strategy=None): - strategy = strategy or get_current_strategy() - Backend = self.get_backend(strategy) - if Backend: - return Backend(strategy=strategy) - - @property - def access_token(self): - """Return access_token stored in extra_data or None""" - return self.extra_data.get('access_token') - - @property - def tokens(self): - warnings.warn('tokens is deprecated, use access_token instead') - return self.access_token - - def refresh_token(self, strategy, *args, **kwargs): - token = self.extra_data.get('refresh_token') or \ - self.extra_data.get('access_token') - backend = self.get_backend(strategy) - if token and backend and hasattr(backend, 'refresh_token'): - backend = backend(strategy=strategy) - response = backend.refresh_token(token, *args, **kwargs) - if self.set_extra_data(response): - self.save() - - def expiration_datetime(self): - """Return provider session live seconds. Returns a timedelta ready to - use with session.set_expiry(). - - If provider returns a timestamp instead of session seconds to live, the - timedelta is inferred from current time (using UTC timezone). None is - returned if there's no value stored or it's invalid. - """ - if self.extra_data and 'expires' in self.extra_data: - try: - expires = int(self.extra_data.get('expires')) - except (ValueError, TypeError): - return None - - now = datetime.utcnow() - - # Detect if expires is a timestamp - if expires > time.mktime(now.timetuple()): - # expires is a datetime - return datetime.fromtimestamp(expires) - now - else: - # expires is a timedelta - return timedelta(seconds=expires) - - def set_extra_data(self, extra_data=None): - if extra_data and self.extra_data != extra_data: - if self.extra_data and not isinstance( - self.extra_data, six.string_types): - self.extra_data.update(extra_data) - else: - self.extra_data = extra_data - return True - - @classmethod - def clean_username(cls, value): - """Clean username removing any unsupported character""" - return CLEAN_USERNAME_REGEX.sub('', value) - - @classmethod - def changed(cls, user): - """The given user instance is ready to be saved""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_username(cls, user): - """Return the username for given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def user_model(cls): - """Return the user model""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def username_max_length(cls): - """Return the max length for username""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - """Return if it's safe to disconnect the social account for the - given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def disconnect(cls, entry): - """Disconnect the social account for the given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def user_exists(cls, *args, **kwargs): - """ - Return True/False if a User instance exists with the given arguments. - Arguments are directly passed to filter() manager method. - """ - raise NotImplementedError('Implement in subclass') - - @classmethod - def create_user(cls, *args, **kwargs): - """Create a user instance""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_user(cls, pk): - """Return user instance for given id""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_users_by_email(cls, email): - """Return users instances for given email address""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_social_auth(cls, provider, uid): - """Return UserSocialAuth for given provider and uid""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_social_auth_for_user(cls, user, provider=None, id=None): - """Return all the UserSocialAuth instances for given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def create_social_auth(cls, user, uid, provider): - """Create a UserSocialAuth instance for given user""" - raise NotImplementedError('Implement in subclass') - - -class NonceMixin(object): - """One use numbers""" - server_url = '' - timestamp = 0 - salt = '' - - @classmethod - def use(cls, server_url, timestamp, salt): - """Create a Nonce instance""" - raise NotImplementedError('Implement in subclass') - - -class AssociationMixin(object): - """OpenId account association""" - server_url = '' - handle = '' - secret = '' - issued = 0 - lifetime = 0 - assoc_type = '' - - @classmethod - def oids(cls, server_url, handle=None): - kwargs = {'server_url': server_url} - if handle is not None: - kwargs['handle'] = handle - return sorted([(assoc.id, cls.openid_association(assoc)) - for assoc in cls.get(**kwargs) - ], key=lambda x: x[1].issued, reverse=True) - - @classmethod - def openid_association(cls, assoc): - secret = assoc.secret - if not isinstance(secret, six.binary_type): - secret = secret.encode() - return OpenIdAssociation(assoc.handle, base64.decodestring(secret), - assoc.issued, assoc.lifetime, - assoc.assoc_type) - - @classmethod - def store(cls, server_url, association): - """Create an Association instance""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get(cls, *args, **kwargs): - """Get an Association instance""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def remove(cls, ids_to_delete): - """Remove an Association instance""" - raise NotImplementedError('Implement in subclass') - - -class CodeMixin(object): - email = '' - code = '' - verified = False - - def verify(self): - self.verified = True - self.save() - - @classmethod - def generate_code(cls): - return uuid.uuid4().hex - - @classmethod - def make_code(cls, email): - code = cls() - code.email = email - code.code = cls.generate_code() - code.verified = False - code.save() - return code - - @classmethod - def get_code(cls, code): - raise NotImplementedError('Implement in subclass') - - -class BaseStorage(object): - user = UserMixin - nonce = NonceMixin - association = AssociationMixin - code = CodeMixin - - @classmethod - def is_integrity_error(cls, exception): - """Check if given exception flags an integrity error in the DB""" - raise NotImplementedError('Implement in subclass') +from social_core.storage import UserMixin, NonceMixin, AssociationMixin, \ + CodeMixin, BaseStorage diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py index e331656c8..6955738b6 100644 --- a/social/storage/django_orm.py +++ b/social/storage/django_orm.py @@ -1,173 +1,2 @@ -"""Django ORM models for Social Auth""" -import base64 -import six -import sys -from django.db import transaction -from django.db.utils import IntegrityError - -from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ - CodeMixin, BaseStorage - - -class DjangoUserMixin(UserMixin): - """Social Auth association model""" - @classmethod - def changed(cls, user): - user.save() - - def set_extra_data(self, extra_data=None): - if super(DjangoUserMixin, self).set_extra_data(extra_data): - self.save() - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - if association_id is not None: - qs = cls.objects.exclude(id=association_id) - else: - qs = cls.objects.exclude(provider=backend_name) - qs = qs.filter(user=user) - - if hasattr(user, 'has_usable_password'): - valid_password = user.has_usable_password() - else: - valid_password = True - return valid_password or qs.count() > 0 - - @classmethod - def disconnect(cls, entry): - entry.delete() - - @classmethod - def username_field(cls): - return getattr(cls.user_model(), 'USERNAME_FIELD', 'username') - - @classmethod - def user_exists(cls, *args, **kwargs): - """ - Return True/False if a User instance exists with the given arguments. - Arguments are directly passed to filter() manager method. - """ - if 'username' in kwargs: - kwargs[cls.username_field()] = kwargs.pop('username') - return cls.user_model().objects.filter(*args, **kwargs).count() > 0 - - @classmethod - def get_username(cls, user): - return getattr(user, cls.username_field(), None) - - @classmethod - def create_user(cls, *args, **kwargs): - username_field = cls.username_field() - if 'username' in kwargs and username_field not in kwargs: - kwargs[username_field] = kwargs.pop('username') - try: - user = cls.user_model().objects.create_user(*args, **kwargs) - except IntegrityError: - # User might have been created on a different thread, try and find them. - # If we don't, re-raise the IntegrityError. - exc_info = sys.exc_info() - # If email comes in as None it won't get found in the get - if kwargs.get('email', True) is None: - kwargs['email'] = '' - try: - user = cls.user_model().objects.get(*args, **kwargs) - except cls.user_model().DoesNotExist: - six.reraise(*exc_info) - return user - - @classmethod - def get_user(cls, pk=None, **kwargs): - if pk: - kwargs = {'pk': pk} - try: - return cls.user_model().objects.get(**kwargs) - except cls.user_model().DoesNotExist: - return None - - @classmethod - def get_users_by_email(cls, email): - user_model = cls.user_model() - email_field = getattr(user_model, 'EMAIL_FIELD', 'email') - return user_model.objects.filter(**{email_field + '__iexact': email}) - - @classmethod - def get_social_auth(cls, provider, uid): - if not isinstance(uid, six.string_types): - uid = str(uid) - try: - return cls.objects.get(provider=provider, uid=uid) - except cls.DoesNotExist: - return None - - @classmethod - def get_social_auth_for_user(cls, user, provider=None, id=None): - qs = user.social_auth.all() - if provider: - qs = qs.filter(provider=provider) - if id: - qs = qs.filter(id=id) - return qs - - @classmethod - def create_social_auth(cls, user, uid, provider): - if not isinstance(uid, six.string_types): - uid = str(uid) - if hasattr(transaction, 'atomic'): - # In Django versions that have an "atomic" transaction decorator / context - # manager, there's a transaction wrapped around this call. - # If the create fails below due to an IntegrityError, ensure that the transaction - # stays undamaged by wrapping the create in an atomic. - with transaction.atomic(): - social_auth = cls.objects.create(user=user, uid=uid, provider=provider) - else: - social_auth = cls.objects.create(user=user, uid=uid, provider=provider) - return social_auth - - -class DjangoNonceMixin(NonceMixin): - @classmethod - def use(cls, server_url, timestamp, salt): - return cls.objects.get_or_create(server_url=server_url, - timestamp=timestamp, - salt=salt)[1] - - -class DjangoAssociationMixin(AssociationMixin): - @classmethod - def store(cls, server_url, association): - # Don't use get_or_create because issued cannot be null - try: - assoc = cls.objects.get(server_url=server_url, - handle=association.handle) - except cls.DoesNotExist: - assoc = cls(server_url=server_url, - handle=association.handle) - assoc.secret = base64.encodestring(association.secret) - assoc.issued = association.issued - assoc.lifetime = association.lifetime - assoc.assoc_type = association.assoc_type - assoc.save() - - @classmethod - def get(cls, *args, **kwargs): - return cls.objects.filter(*args, **kwargs) - - @classmethod - def remove(cls, ids_to_delete): - cls.objects.filter(pk__in=ids_to_delete).delete() - - -class DjangoCodeMixin(CodeMixin): - @classmethod - def get_code(cls, code): - try: - return cls.objects.get(code=code) - except cls.DoesNotExist: - return None - - -class BaseDjangoStorage(BaseStorage): - user = DjangoUserMixin - nonce = DjangoNonceMixin - association = DjangoAssociationMixin - code = DjangoCodeMixin +from social_django.storage import DjangoUserMixin, DjangoNonceMixin, \ + DjangoAssociationMixin, DjangoCodeMixin, BaseDjangoStorage diff --git a/social/storage/mongoengine_orm.py b/social/storage/mongoengine_orm.py index e74f1f1c2..beebf7372 100644 --- a/social/storage/mongoengine_orm.py +++ b/social/storage/mongoengine_orm.py @@ -1,188 +1,3 @@ -import base64 -import six - -from mongoengine import DictField, IntField, StringField, \ - EmailField, BooleanField -from mongoengine.queryset import OperationError - -from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ - CodeMixin, BaseStorage - - -UNUSABLE_PASSWORD = '!' # Borrowed from django 1.4 - - -class MongoengineUserMixin(UserMixin): - """Social Auth association model""" - user = None - provider = StringField(max_length=32) - uid = StringField(max_length=255, unique_with='provider') - extra_data = DictField() - - def str_id(self): - return str(self.id) - - @classmethod - def get_social_auth_for_user(cls, user, provider=None, id=None): - qs = cls.objects - if provider: - qs = qs.filter(provider=provider) - if id: - qs = qs.filter(id=id) - return qs.filter(user=user.id) - - @classmethod - def create_social_auth(cls, user, uid, provider): - if not isinstance(type(uid), six.string_types): - uid = str(uid) - return cls.objects.create(user=user.id, uid=uid, provider=provider) - - @classmethod - def username_max_length(cls): - username_field = cls.username_field() - field = getattr(cls.user_model(), username_field) - return field.max_length - - @classmethod - def username_field(cls): - return getattr(cls.user_model(), 'USERNAME_FIELD', 'username') - - @classmethod - def create_user(cls, *args, **kwargs): - kwargs['password'] = UNUSABLE_PASSWORD - if 'email' in kwargs: - # Empty string makes email regex validation fail - kwargs['email'] = kwargs['email'] or None - return cls.user_model().objects.create(*args, **kwargs) - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - if association_id is not None: - qs = cls.objects.filter(id__ne=association_id) - else: - qs = cls.objects.filter(provider__ne=backend_name) - qs = qs.filter(user=user) - - if hasattr(user, 'has_usable_password'): - valid_password = user.has_usable_password() - else: - valid_password = True - - return valid_password or qs.count() > 0 - - @classmethod - def changed(cls, user): - user.save() - - def set_extra_data(self, extra_data=None): - if super(MongoengineUserMixin, self).set_extra_data(extra_data): - self.save() - - @classmethod - def disconnect(cls, entry): - entry.delete() - - @classmethod - def user_exists(cls, *args, **kwargs): - """ - Return True/False if a User instance exists with the given arguments. - Arguments are directly passed to filter() manager method. - """ - if 'username' in kwargs: - kwargs[cls.username_field()] = kwargs.pop('username') - return cls.user_model().objects.filter(*args, **kwargs).count() > 0 - - @classmethod - def get_username(cls, user): - return getattr(user, cls.username_field(), None) - - @classmethod - def get_user(cls, pk): - try: - return cls.user_model().objects.get(id=pk) - except cls.user_model().DoesNotExist: - return None - - @classmethod - def get_users_by_email(cls, email): - return cls.user_model().objects.filter(email__iexact=email) - - @classmethod - def get_social_auth(cls, provider, uid): - if not isinstance(uid, six.string_types): - uid = str(uid) - try: - return cls.objects.get(provider=provider, uid=uid) - except cls.DoesNotExist: - return None - - -class MongoengineNonceMixin(NonceMixin): - """One use numbers""" - server_url = StringField(max_length=255) - timestamp = IntField() - salt = StringField(max_length=40) - - @classmethod - def use(cls, server_url, timestamp, salt): - return cls.objects.get_or_create(server_url=server_url, - timestamp=timestamp, - salt=salt)[1] - - -class MongoengineAssociationMixin(AssociationMixin): - """OpenId account association""" - server_url = StringField(max_length=255) - handle = StringField(max_length=255) - secret = StringField(max_length=255) # Stored base64 encoded - issued = IntField() - lifetime = IntField() - assoc_type = StringField(max_length=64) - - @classmethod - def store(cls, server_url, association): - # Don't use get_or_create because issued cannot be null - try: - assoc = cls.objects.get(server_url=server_url, - handle=association.handle) - except cls.DoesNotExist: - assoc = cls(server_url=server_url, - handle=association.handle) - assoc.secret = base64.encodestring(association.secret) - assoc.issued = association.issued - assoc.lifetime = association.lifetime - assoc.assoc_type = association.assoc_type - assoc.save() - - @classmethod - def get(cls, *args, **kwargs): - return cls.objects.filter(*args, **kwargs) - - @classmethod - def remove(cls, ids_to_delete): - cls.objects.filter(pk__in=ids_to_delete).delete() - - -class MongoengineCodeMixin(CodeMixin): - email = EmailField() - code = StringField(max_length=32) - verified = BooleanField(default=False) - - @classmethod - def get_code(cls, code): - try: - return cls.objects.get(code=code) - except cls.DoesNotExist: - return None - - -class BaseMongoengineStorage(BaseStorage): - user = MongoengineUserMixin - nonce = MongoengineNonceMixin - association = MongoengineAssociationMixin - code = MongoengineCodeMixin - - @classmethod - def is_integrity_error(cls, exception): - return exception.__class__ is OperationError and \ - 'E11000' in exception.message +from social_mongoengine.storage import MongoengineUserMixin, \ + MongoengineNonceMixin, MongoengineAssociationMixin, \ + MongoengineCodeMixin, BaseMongoengineStorage diff --git a/social/storage/peewee_orm.py b/social/storage/peewee_orm.py index 60b5da0f2..6be85cbff 100644 --- a/social/storage/peewee_orm.py +++ b/social/storage/peewee_orm.py @@ -1,199 +1,2 @@ -import six -import base64 - -from peewee import CharField, Model, Proxy, IntegrityError -from playhouse.kv import JSONField - -from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ - CodeMixin, BaseStorage - - -def get_query_by_dict_param(cls, params): - query = True - - for field_name, value in params.iteritems(): - query_item = cls._meta.fields[field_name] == value - query = query & query_item - return query - - -database_proxy = Proxy() - - -class BaseModel(Model): - class Meta: - database = database_proxy - - -class PeeweeUserMixin(UserMixin, BaseModel): - provider = CharField() - extra_data = JSONField(null=True) - uid = CharField() - user = None - - @classmethod - def changed(cls, user): - user.save() - - def set_extra_data(self, extra_data=None): - if super(PeeweeUserMixin, self).set_extra_data(extra_data): - self.save() - - @classmethod - def username_max_length(cls): - username_field = cls.username_field() - field = getattr(cls.user_model(), username_field) - return field.max_length - - @classmethod - def username_field(cls): - return getattr(cls.user_model(), 'USERNAME_FIELD', 'username') - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - if association_id is not None: - query = cls.select().where(cls.id != association_id) - else: - query = cls.select().where(cls.provider != backend_name) - query = query.where(cls.user == user) - - if hasattr(user, 'has_usable_password'): - valid_password = user.has_usable_password() - else: - valid_password = True - return valid_password or query.count() > 0 - - @classmethod - def disconnect(cls, entry): - entry.delete_instance() - - @classmethod - def user_exists(cls, *args, **kwargs): - """ - Return True/False if a User instance exists with the given arguments. - """ - user_model = cls.user_model() - query = get_query_by_dict_param(user_model, kwargs) - return user_model.select().where(query).count() > 0 - - @classmethod - def get_username(cls, user): - return getattr(user, cls.username_field(), None) - - @classmethod - def create_user(cls, *args, **kwargs): - username_field = cls.username_field() - if 'username' in kwargs and username_field not in kwargs: - kwargs[username_field] = kwargs.pop('username') - return cls.user_model().create(*args, **kwargs) - - @classmethod - def get_user(cls, pk, **kwargs): - if pk: - kwargs = {'id': pk} - try: - return cls.user_model().select().get( - get_query_by_dict_param(cls.user_model(), kwargs) - ) - except cls.user_model().DoesNotExist: - return None - - @classmethod - def get_users_by_email(cls, email): - user_model = cls.user_model() - return user_model.select().where(user_model.email == email) - - @classmethod - def get_social_auth(cls, provider, uid): - if not isinstance(uid, six.string_types): - uid = str(uid) - try: - return cls.select().where( - cls.provider == provider, cls.uid == uid - ).get() - except cls.DoesNotExist: - return None - - @classmethod - def get_social_auth_for_user(cls, user, provider=None, id=None): - query = cls.select().where(cls.user == user) - if provider: - query = query.where(cls.provider == provider) - if id: - query = query.where(cls.id == id) - return list(query) - - @classmethod - def create_social_auth(cls, user, uid, provider): - if not isinstance(uid, six.string_types): - uid = str(uid) - return cls.create(user=user, uid=uid, provider=provider) - - -class PeeweeNonceMixin(NonceMixin, BaseModel): - server_url = CharField() - timestamp = CharField() - salt = CharField() - - @classmethod - def use(cls, server_url, timestamp, salt): - return cls.select().get_or_create(cls.server_url == server_url, - cls.timestamp == timestamp, - cls.salt == salt) - - -class PeeweeAssociationMixin(AssociationMixin, BaseModel): - server_url = CharField() - handle = CharField() - secret = CharField() # base64 encoded - issued = CharField() - lifetime = CharField() - assoc_type = CharField() - - @classmethod - def store(cls, server_url, association): - try: - assoc = cls.select().get(cls.server_url == server_url, - cls.handle == association.handle) - except cls.DoesNotExist: - assoc = cls(server_url=server_url, - handle=association.handle) - - assoc.secret = base64.encodestring(association.secret) - assoc.issued = association.issued - assoc.lifetime = association.lifetime - assoc.assoc_type = association.assoc_type - assoc.save() - - @classmethod - def get(cls, *args, **kwargs): - query = get_query_by_dict_param(cls, kwargs) - return cls.select().where(query) - - @classmethod - def remove(cls, ids_to_delete): - cls.select().where(cls.id << ids_to_delete).delete() - - -class PeeweeCodeMixin(CodeMixin, BaseModel): - email = CharField() - code = CharField() # base64 encoded - issued = CharField() - - @classmethod - def get_code(cls, code): - try: - return cls.select().get(cls.code == code) - except cls.DoesNotExist: - return None - - -class BasePeeweeStorage(BaseStorage): - user = PeeweeUserMixin - nonce = PeeweeNonceMixin - association = PeeweeAssociationMixin - code = PeeweeCodeMixin - - @classmethod - def is_integrity_error(cls, exception): - return exception.__class__ is IntegrityError +from social_peewee.storage import database_proxy, BaseModel, PeeweeUserMixin, \ + PeeweeNonceMixin, PeeweeAssociationMixin, PeeweeCodeMixin, BasePeeweeStorage diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py index e48ef64cf..862af8d44 100644 --- a/social/storage/sqlalchemy_orm.py +++ b/social/storage/sqlalchemy_orm.py @@ -1,237 +1,4 @@ -"""SQLAlchemy models for Social Auth""" -import base64 -import six -import json - -try: - import transaction -except ImportError: - transaction = None - -from sqlalchemy import Column, Integer, String -from sqlalchemy.exc import IntegrityError -from sqlalchemy.types import PickleType, Text -from sqlalchemy.schema import UniqueConstraint -from sqlalchemy.ext.mutable import MutableDict - -from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \ - CodeMixin, BaseStorage - - -# JSON type field -class JSONType(PickleType): - impl = Text - - def __init__(self, *args, **kwargs): - kwargs['pickler'] = json - super(JSONType, self).__init__(*args, **kwargs) - - -class SQLAlchemyMixin(object): - COMMIT_SESSION = True - - @classmethod - def _session(cls): - raise NotImplementedError('Implement in subclass') - - @classmethod - def _query(cls): - return cls._session().query(cls) - - @classmethod - def _new_instance(cls, model, *args, **kwargs): - return cls._save_instance(model(*args, **kwargs)) - - @classmethod - def _save_instance(cls, instance): - cls._session().add(instance) - if cls.COMMIT_SESSION: - cls._session().commit() - cls._session().flush() - else: - cls._flush() - return instance - - @classmethod - def _flush(cls): - try: - cls._session().flush() - except AssertionError: - if transaction: - with transaction.manager as manager: - manager.commit() - else: - cls._session().commit() - - def save(self): - self._save_instance(self) - - -class SQLAlchemyUserMixin(SQLAlchemyMixin, UserMixin): - """Social Auth association model""" - __tablename__ = 'social_auth_usersocialauth' - __table_args__ = (UniqueConstraint('provider', 'uid'),) - id = Column(Integer, primary_key=True) - provider = Column(String(32)) - extra_data = Column(MutableDict.as_mutable(JSONType)) - uid = None - user_id = None - user = None - - @classmethod - def changed(cls, user): - cls._save_instance(user) - - def set_extra_data(self, extra_data=None): - if super(SQLAlchemyUserMixin, self).set_extra_data(extra_data): - self._save_instance(self) - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - if association_id is not None: - qs = cls._query().filter(cls.id != association_id) - else: - qs = cls._query().filter(cls.provider != backend_name) - qs = qs.filter(cls.user == user) - - if hasattr(user, 'has_usable_password'): # TODO - valid_password = user.has_usable_password() - else: - valid_password = True - return valid_password or qs.count() > 0 - - @classmethod - def disconnect(cls, entry): - cls._session().delete(entry) - cls._flush() - - @classmethod - def user_query(cls): - return cls._session().query(cls.user_model()) - - @classmethod - def user_exists(cls, *args, **kwargs): - """ - Return True/False if a User instance exists with the given arguments. - Arguments are directly passed to filter() manager method. - """ - return cls.user_query().filter_by(*args, **kwargs).count() > 0 - - @classmethod - def get_username(cls, user): - return getattr(user, 'username', None) - - @classmethod - def create_user(cls, *args, **kwargs): - return cls._new_instance(cls.user_model(), *args, **kwargs) - - @classmethod - def get_user(cls, pk): - return cls.user_query().get(pk) - - @classmethod - def get_users_by_email(cls, email): - return cls.user_query().filter_by(email=email) - - @classmethod - def get_social_auth(cls, provider, uid): - if not isinstance(uid, six.string_types): - uid = str(uid) - try: - return cls._query().filter_by(provider=provider, - uid=uid)[0] - except IndexError: - return None - - @classmethod - def get_social_auth_for_user(cls, user, provider=None, id=None): - qs = cls._query().filter_by(user_id=user.id) - if provider: - qs = qs.filter_by(provider=provider) - if id: - qs = qs.filter_by(id=id) - return qs - - @classmethod - def create_social_auth(cls, user, uid, provider): - if not isinstance(uid, six.string_types): - uid = str(uid) - return cls._new_instance(cls, user=user, uid=uid, provider=provider) - - -class SQLAlchemyNonceMixin(SQLAlchemyMixin, NonceMixin): - __tablename__ = 'social_auth_nonce' - __table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - timestamp = Column(Integer) - salt = Column(String(40)) - - @classmethod - def use(cls, server_url, timestamp, salt): - kwargs = {'server_url': server_url, 'timestamp': timestamp, - 'salt': salt} - try: - return cls._query().filter_by(**kwargs)[0] - except IndexError: - return cls._new_instance(cls, **kwargs) - - -class SQLAlchemyAssociationMixin(SQLAlchemyMixin, AssociationMixin): - __tablename__ = 'social_auth_association' - __table_args__ = (UniqueConstraint('server_url', 'handle'),) - id = Column(Integer, primary_key=True) - server_url = Column(String(255)) - handle = Column(String(255)) - secret = Column(String(255)) # base64 encoded - issued = Column(Integer) - lifetime = Column(Integer) - assoc_type = Column(String(64)) - - @classmethod - def store(cls, server_url, association): - # Don't use get_or_create because issued cannot be null - try: - assoc = cls._query().filter_by(server_url=server_url, - handle=association.handle)[0] - except IndexError: - assoc = cls(server_url=server_url, - handle=association.handle) - assoc.secret = base64.encodestring(association.secret).decode() - assoc.issued = association.issued - assoc.lifetime = association.lifetime - assoc.assoc_type = association.assoc_type - cls._save_instance(assoc) - - @classmethod - def get(cls, *args, **kwargs): - return cls._query().filter_by(*args, **kwargs) - - @classmethod - def remove(cls, ids_to_delete): - cls._query().filter(cls.id.in_(ids_to_delete)).delete( - synchronize_session='fetch' - ) - - -class SQLAlchemyCodeMixin(SQLAlchemyMixin, CodeMixin): - __tablename__ = 'social_auth_code' - __table_args__ = (UniqueConstraint('code', 'email'),) - id = Column(Integer, primary_key=True) - email = Column(String(200)) - code = Column(String(32), index=True) - - @classmethod - def get_code(cls, code): - return cls._query().filter(cls.code == code).first() - - -class BaseSQLAlchemyStorage(BaseStorage): - user = SQLAlchemyUserMixin - nonce = SQLAlchemyNonceMixin - association = SQLAlchemyAssociationMixin - code = SQLAlchemyCodeMixin - - @classmethod - def is_integrity_error(cls, exception): - return exception.__class__ is IntegrityError +from social_sqlalchemy.storage import JSONType, SQLAlchemyMixin, \ + SQLAlchemyUserMixin, SQLAlchemyNonceMixin, \ + SQLAlchemyAssociationMixin, SQLAlchemyCodeMixin, \ + BaseSQLAlchemyStorage diff --git a/social/store.py b/social/store.py index e3275a2d3..fa1866626 100644 --- a/social/store.py +++ b/social/store.py @@ -1,84 +1 @@ -import time - -try: - import cPickle as pickle -except ImportError: - import pickle - -from openid.store.interface import OpenIDStore as BaseOpenIDStore -from openid.store.nonce import SKEW - - -class OpenIdStore(BaseOpenIDStore): - """Storage class""" - def __init__(self, strategy): - """Init method""" - super(OpenIdStore, self).__init__() - self.strategy = strategy - self.storage = strategy.storage - self.assoc = self.storage.association - self.nonce = self.storage.nonce - self.max_nonce_age = 6 * 60 * 60 # Six hours - - def storeAssociation(self, server_url, association): - """Store new assocition if doesn't exist""" - self.assoc.store(server_url, association) - - def removeAssociation(self, server_url, handle): - """Remove association""" - associations_ids = list(dict(self.assoc.oids(server_url, - handle)).keys()) - if associations_ids: - self.assoc.remove(associations_ids) - - def expiresIn(self, assoc): - if hasattr(assoc, 'getExpiresIn'): - return assoc.getExpiresIn() - else: # python3-openid 3.0.2 - return assoc.expiresIn - - def getAssociation(self, server_url, handle=None): - """Return stored assocition""" - associations, expired = [], [] - for assoc_id, association in self.assoc.oids(server_url, handle): - expires = self.expiresIn(association) - if expires > 0: - associations.append(association) - elif expires == 0: - expired.append(assoc_id) - - if expired: # clear expired associations - self.assoc.remove(expired) - - if associations: # return most recet association - return associations[0] - - def useNonce(self, server_url, timestamp, salt): - """Generate one use number and return *if* it was created""" - if abs(timestamp - time.time()) > SKEW: - return False - return self.nonce.use(server_url, timestamp, salt) - - -class OpenIdSessionWrapper(dict): - pickle_instances = ( - '_yadis_services__openid_consumer_', - '_openid_consumer_last_token' - ) - - def __getitem__(self, name): - value = super(OpenIdSessionWrapper, self).__getitem__(name) - if name in self.pickle_instances: - value = pickle.loads(value) - return value - - def __setitem__(self, name, value): - if name in self.pickle_instances: - value = pickle.dumps(value, 0) - super(OpenIdSessionWrapper, self).__setitem__(name, value) - - def get(self, name, default=None): - try: - return self[name] - except KeyError: - return default +from social_core.store import OpenIdStore, OpenIdSessionWrapper diff --git a/social/strategies/base.py b/social/strategies/base.py index 22f24e195..fcc4677cb 100644 --- a/social/strategies/base.py +++ b/social/strategies/base.py @@ -1,212 +1 @@ -import time -import random -import hashlib - -from social.utils import setting_name, module_member -from social.store import OpenIdStore, OpenIdSessionWrapper -from social.pipeline import DEFAULT_AUTH_PIPELINE, DEFAULT_DISCONNECT_PIPELINE -from social.pipeline.utils import partial_from_session, partial_to_session - - -class BaseTemplateStrategy(object): - def __init__(self, strategy): - self.strategy = strategy - - def render(self, tpl=None, html=None, context=None): - if not tpl and not html: - raise ValueError('Missing template or html parameters') - context = context or {} - if tpl: - return self.render_template(tpl, context) - else: - return self.render_string(html, context) - - def render_template(self, tpl, context): - raise NotImplementedError('Implement in subclass') - - def render_string(self, html, context): - raise NotImplementedError('Implement in subclass') - - -class BaseStrategy(object): - ALLOWED_CHARS = 'abcdefghijklmnopqrstuvwxyz' \ - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \ - '0123456789' - DEFAULT_TEMPLATE_STRATEGY = BaseTemplateStrategy - - def __init__(self, storage=None, tpl=None): - self.storage = storage - self.tpl = (tpl or self.DEFAULT_TEMPLATE_STRATEGY)(self) - - def setting(self, name, default=None, backend=None): - names = [setting_name(name), name] - if backend: - names.insert(0, setting_name(backend.name, name)) - for name in names: - try: - return self.get_setting(name) - except (AttributeError, KeyError): - pass - return default - - def create_user(self, *args, **kwargs): - return self.storage.user.create_user(*args, **kwargs) - - def get_user(self, *args, **kwargs): - return self.storage.user.get_user(*args, **kwargs) - - def session_setdefault(self, name, value): - self.session_set(name, value) - return self.session_get(name) - - def openid_session_dict(self, name): - # Many frameworks are switching the session serialization from Pickle - # to JSON to avoid code execution risks. Flask did this from Flask - # 0.10, Django is switching to JSON by default from version 1.6. - # - # Sadly python-openid stores classes instances in the session which - # fails the JSON serialization, the classes are: - # - # openid.yadis.manager.YadisServiceManager - # openid.consumer.discover.OpenIDServiceEndpoint - # - # This method will return a wrapper over the session value used with - # openid (a dict) which will automatically keep a pickled value for the - # mentioned classes. - return OpenIdSessionWrapper(self.session_setdefault(name, {})) - - def to_session_value(self, val): - return val - - def from_session_value(self, val): - return val - - def partial_to_session(self, next, backend, request=None, *args, **kwargs): - return partial_to_session(self, next, backend, request=request, - *args, **kwargs) - - def partial_from_session(self, session): - return partial_from_session(self, session) - - def clean_partial_pipeline(self, name='partial_pipeline'): - self.session_pop(name) - - def openid_store(self): - return OpenIdStore(self) - - def get_pipeline(self): - return self.setting('PIPELINE', DEFAULT_AUTH_PIPELINE) - - def get_disconnect_pipeline(self): - return self.setting('DISCONNECT_PIPELINE', DEFAULT_DISCONNECT_PIPELINE) - - def random_string(self, length=12, chars=ALLOWED_CHARS): - # Implementation borrowed from django 1.4 - try: - random.SystemRandom() - except NotImplementedError: - key = self.setting('SECRET_KEY', '') - seed = '{0}{1}{2}'.format(random.getstate(), time.time(), key) - random.seed(hashlib.sha256(seed.encode()).digest()) - return ''.join([random.choice(chars) for i in range(length)]) - - def absolute_uri(self, path=None): - uri = self.build_absolute_uri(path) - if uri and self.setting('REDIRECT_IS_HTTPS'): - uri = uri.replace('http://', 'https://') - return uri - - def get_language(self): - """Return current language""" - return '' - - def send_email_validation(self, backend, email): - email_validation = self.setting('EMAIL_VALIDATION_FUNCTION') - send_email = module_member(email_validation) - code = self.storage.code.make_code(email) - send_email(self, backend, code) - return code - - def validate_email(self, email, code): - verification_code = self.storage.code.get_code(code) - if not verification_code or verification_code.code != code: - return False - elif verification_code.email != email: - return False - else: - verification_code.verify() - return True - - def render_html(self, tpl=None, html=None, context=None): - """Render given template or raw html with given context""" - return self.tpl.render(tpl, html, context) - - def authenticate(self, backend, *args, **kwargs): - """Trigger the authentication mechanism tied to the current - framework""" - kwargs['strategy'] = self - kwargs['storage'] = self.storage - kwargs['backend'] = backend - return backend.authenticate(*args, **kwargs) - - def get_backends(self): - """Return configured backends""" - return self.setting('AUTHENTICATION_BACKENDS', []) - - # Implement the following methods on strategies sub-classes - - def redirect(self, url): - """Return a response redirect to the given URL""" - raise NotImplementedError('Implement in subclass') - - def get_setting(self, name): - """Return value for given setting name""" - raise NotImplementedError('Implement in subclass') - - def html(self, content): - """Return HTTP response with given content""" - raise NotImplementedError('Implement in subclass') - - def request_data(self, merge=True): - """Return current request data (POST or GET)""" - raise NotImplementedError('Implement in subclass') - - def request_host(self): - """Return current host value""" - raise NotImplementedError('Implement in subclass') - - def session_get(self, name, default=None): - """Return session value for given key""" - raise NotImplementedError('Implement in subclass') - - def session_set(self, name, value): - """Set session value for given key""" - raise NotImplementedError('Implement in subclass') - - def session_pop(self, name): - """Pop session value for given key""" - raise NotImplementedError('Implement in subclass') - - def build_absolute_uri(self, path=None): - """Build absolute URI with given (optional) path""" - raise NotImplementedError('Implement in subclass') - - def request_is_secure(self): - """Is the request using HTTPS?""" - raise NotImplementedError('Implement in subclass') - - def request_path(self): - """path of the current request""" - raise NotImplementedError('Implement in subclass') - - def request_port(self): - """Port in use for this request""" - raise NotImplementedError('Implement in subclass') - - def request_get(self): - """Request GET data""" - raise NotImplementedError('Implement in subclass') - - def request_post(self): - """Request POST data""" - raise NotImplementedError('Implement in subclass') +from social_core.strategy import BaseTemplateStrategy, BaseStrategy diff --git a/social/strategies/cherrypy_strategy.py b/social/strategies/cherrypy_strategy.py index d0e9e41f2..6634b1ea0 100644 --- a/social/strategies/cherrypy_strategy.py +++ b/social/strategies/cherrypy_strategy.py @@ -1,66 +1,2 @@ -import six -import cherrypy - -from social.strategies.base import BaseStrategy, BaseTemplateStrategy - - -class CherryPyJinja2TemplateStrategy(BaseTemplateStrategy): - def __init__(self, strategy): - self.strategy = strategy - self.env = cherrypy.tools.jinja2env - - def render_template(self, tpl, context): - return self.env.get_template(tpl).render(context) - - def render_string(self, html, context): - return self.env.from_string(html).render(context) - - -class CherryPyStrategy(BaseStrategy): - DEFAULT_TEMPLATE_STRATEGY = CherryPyJinja2TemplateStrategy - - def get_setting(self, name): - return cherrypy.config[name] - - def request_data(self, merge=True): - if merge: - data = cherrypy.request.params - elif cherrypy.request.method == 'POST': - data = cherrypy.body.params - else: - data = cherrypy.request.params - return data - - def request_host(self): - return cherrypy.request.base - - def redirect(self, url): - raise cherrypy.HTTPRedirect(url) - - def html(self, content): - return content - - def authenticate(self, backend, *args, **kwargs): - kwargs['strategy'] = self - kwargs['storage'] = self.storage - kwargs['backend'] = backend - return backend.authenticate(*args, **kwargs) - - def session_get(self, name, default=None): - return cherrypy.session.get(name, default) - - def session_set(self, name, value): - cherrypy.session[name] = value - - def session_pop(self, name): - cherrypy.session.pop(name, None) - - def session_setdefault(self, name, value): - return cherrypy.session.setdefault(name, value) - - def build_absolute_uri(self, path=None): - return cherrypy.url(path or '') - - def is_response(self, value): - return isinstance(value, six.string_types) or \ - isinstance(value, cherrypy.CherryPyException) +from social_cherrypy.strategy import CherryPyJinja2TemplateStrategy, \ + CherryPyStrategy diff --git a/social/strategies/django_strategy.py b/social/strategies/django_strategy.py index 2cfc82842..0f7617fb7 100644 --- a/social/strategies/django_strategy.py +++ b/social/strategies/django_strategy.py @@ -1,146 +1 @@ -from django.conf import settings -from django.http import HttpResponse -from django.db.models import Model -from django.contrib.contenttypes.models import ContentType -from django.contrib.auth import authenticate -from django.shortcuts import redirect -from django.template import TemplateDoesNotExist, RequestContext, loader -from django.utils.encoding import force_text -from django.utils.functional import Promise -from django.utils.translation import get_language - -from social.strategies.base import BaseStrategy, BaseTemplateStrategy - - -class DjangoTemplateStrategy(BaseTemplateStrategy): - def render_template(self, tpl, context): - template = loader.get_template(tpl) - return template.render(RequestContext(self.strategy.request, context)) - - def render_string(self, html, context): - template = loader.get_template_from_string(html) - return template.render(RequestContext(self.strategy.request, context)) - - -class DjangoStrategy(BaseStrategy): - DEFAULT_TEMPLATE_STRATEGY = DjangoTemplateStrategy - - def __init__(self, storage, request=None, tpl=None): - self.request = request - self.session = request.session if request else {} - super(DjangoStrategy, self).__init__(storage, tpl) - - def get_setting(self, name): - value = getattr(settings, name) - # Force text on URL named settings that are instance of Promise - if name.endswith('_URL') and isinstance(value, Promise): - value = force_text(value) - return value - - def request_data(self, merge=True): - if not self.request: - return {} - if merge: - data = self.request.GET.copy() - data.update(self.request.POST) - elif self.request.method == 'POST': - data = self.request.POST - else: - data = self.request.GET - return data - - def request_host(self): - if self.request: - return self.request.get_host() - - def request_is_secure(self): - """Is the request using HTTPS?""" - return self.request.is_secure() - - def request_path(self): - """path of the current request""" - return self.request.path - - def request_port(self): - """Port in use for this request""" - return self.request.META['SERVER_PORT'] - - def request_get(self): - """Request GET data""" - return self.request.GET.copy() - - def request_post(self): - """Request POST data""" - return self.request.POST.copy() - - def redirect(self, url): - return redirect(url) - - def html(self, content): - return HttpResponse(content, content_type='text/html;charset=UTF-8') - - def render_html(self, tpl=None, html=None, context=None): - if not tpl and not html: - raise ValueError('Missing template or html parameters') - context = context or {} - try: - template = loader.get_template(tpl) - except TemplateDoesNotExist: - template = loader.get_template_from_string(html) - return template.render(RequestContext(self.request, context)) - - def authenticate(self, backend, *args, **kwargs): - kwargs['strategy'] = self - kwargs['storage'] = self.storage - kwargs['backend'] = backend - return authenticate(*args, **kwargs) - - def session_get(self, name, default=None): - return self.session.get(name, default) - - def session_set(self, name, value): - self.session[name] = value - if hasattr(self.session, 'modified'): - self.session.modified = True - - def session_pop(self, name): - return self.session.pop(name, None) - - def session_setdefault(self, name, value): - return self.session.setdefault(name, value) - - def build_absolute_uri(self, path=None): - if self.request: - return self.request.build_absolute_uri(path) - else: - return path - - def random_string(self, length=12, chars=BaseStrategy.ALLOWED_CHARS): - try: - from django.utils.crypto import get_random_string - except ImportError: # django < 1.4 - return super(DjangoStrategy, self).random_string(length, chars) - else: - return get_random_string(length, chars) - - def to_session_value(self, val): - """Converts values that are instance of Model to a dictionary - with enough information to retrieve the instance back later.""" - if isinstance(val, Model): - val = { - 'pk': val.pk, - 'ctype': ContentType.objects.get_for_model(val).pk - } - return val - - def from_session_value(self, val): - """Converts back the instance saved by self._ctype function.""" - if isinstance(val, dict) and 'pk' in val and 'ctype' in val: - ctype = ContentType.objects.get_for_id(val['ctype']) - ModelClass = ctype.model_class() - val = ModelClass.objects.get(pk=val['pk']) - return val - - def get_language(self): - """Return current language""" - return get_language() +from social_django.strategy import DjangoTemplateStrategy, DjangoStrategy diff --git a/social/strategies/flask_strategy.py b/social/strategies/flask_strategy.py index d8ebfeaba..cc9d2e6ed 100644 --- a/social/strategies/flask_strategy.py +++ b/social/strategies/flask_strategy.py @@ -1,56 +1 @@ -from flask import current_app, request, redirect, make_response, session, \ - render_template, render_template_string - -from social.utils import build_absolute_uri -from social.strategies.base import BaseStrategy, BaseTemplateStrategy - - -class FlaskTemplateStrategy(BaseTemplateStrategy): - def render_template(self, tpl, context): - return render_template(tpl, **context) - - def render_string(self, html, context): - return render_template_string(html, **context) - - -class FlaskStrategy(BaseStrategy): - DEFAULT_TEMPLATE_STRATEGY = FlaskTemplateStrategy - - def get_setting(self, name): - return current_app.config[name] - - def request_data(self, merge=True): - if merge: - data = request.form.copy() - data.update(request.args) - elif request.method == 'POST': - data = request.form - else: - data = request.args - return data - - def request_host(self): - return request.host - - def redirect(self, url): - return redirect(url) - - def html(self, content): - response = make_response(content) - response.headers['Content-Type'] = 'text/html;charset=UTF-8' - return response - - def session_get(self, name, default=None): - return session.get(name, default) - - def session_set(self, name, value): - session[name] = value - - def session_pop(self, name): - return session.pop(name, None) - - def session_setdefault(self, name, value): - return session.setdefault(name, value) - - def build_absolute_uri(self, path=None): - return build_absolute_uri(request.host_url, path) +from social_flask.strategy import FlaskTemplateStrategy, FlaskStrategy diff --git a/social/strategies/pyramid_strategy.py b/social/strategies/pyramid_strategy.py index 9761a42b7..8328305e1 100644 --- a/social/strategies/pyramid_strategy.py +++ b/social/strategies/pyramid_strategy.py @@ -1,74 +1 @@ -from webob.multidict import NoVars - -from pyramid.response import Response -from pyramid.httpexceptions import HTTPFound -from pyramid.renderers import render - -from social.utils import build_absolute_uri -from social.strategies.base import BaseStrategy, BaseTemplateStrategy - - -class PyramidTemplateStrategy(BaseTemplateStrategy): - def render_template(self, tpl, context): - return render(tpl, context, request=self.strategy.request) - - def render_string(self, html, context): - return render(html, context, request=self.strategy.request) - - -class PyramidStrategy(BaseStrategy): - DEFAULT_TEMPLATE_STRATEGY = PyramidTemplateStrategy - - def __init__(self, storage, request, tpl=None): - self.request = request - super(PyramidStrategy, self).__init__(storage, tpl) - - def redirect(self, url): - """Return a response redirect to the given URL""" - response = getattr(self.request, 'response', None) - if response is None: - response = HTTPFound(location=url) - else: - response = HTTPFound(location=url, headers=response.headers) - return response - - def get_setting(self, name): - """Return value for given setting name""" - return self.request.registry.settings[name] - - def html(self, content): - """Return HTTP response with given content""" - return Response(body=content) - - def request_data(self, merge=True): - """Return current request data (POST or GET)""" - if self.request.method == 'POST': - if merge: - data = self.request.POST.copy() - if not isinstance(self.request.GET, NoVars): - data.update(self.request.GET) - else: - data = self.request.POST - else: - data = self.request.GET - return data - - def request_host(self): - """Return current host value""" - return self.request.host - - def session_get(self, name, default=None): - """Return session value for given key""" - return self.request.session.get(name, default) - - def session_set(self, name, value): - """Set session value for given key""" - self.request.session[name] = value - - def session_pop(self, name): - """Pop session value for given key""" - return self.request.session.pop(name, None) - - def build_absolute_uri(self, path=None): - """Build absolute URI with given (optional) path""" - return build_absolute_uri(self.request.host_url, path) +from social_pyramid.strategy import PyramidTemplateStrategy, PyramidStrategy diff --git a/social/strategies/tornado_strategy.py b/social/strategies/tornado_strategy.py index afb8c1c9e..24c5c7089 100644 --- a/social/strategies/tornado_strategy.py +++ b/social/strategies/tornado_strategy.py @@ -1,75 +1 @@ -import json -import six - -from tornado.template import Loader, Template - -from social.utils import build_absolute_uri -from social.strategies.base import BaseStrategy, BaseTemplateStrategy - - -class TornadoTemplateStrategy(BaseTemplateStrategy): - def render_template(self, tpl, context): - path, tpl = tpl.rsplit('/', 1) - return Loader(path).load(tpl).generate(**context) - - def render_string(self, html, context): - return Template(html).generate(**context) - - -class TornadoStrategy(BaseStrategy): - DEFAULT_TEMPLATE_STRATEGY = TornadoTemplateStrategy - - def __init__(self, storage, request_handler, tpl=None): - self.request_handler = request_handler - self.request = self.request_handler.request - super(TornadoStrategy, self).__init__(storage, tpl) - - def get_setting(self, name): - return self.request_handler.settings[name] - - def request_data(self, merge=True): - # Multiple valued arguments not supported yet - return dict((key, val[0].decode()) - for key, val in six.iteritems(self.request.arguments)) - - def request_host(self): - return self.request.host - - def redirect(self, url): - return self.request_handler.redirect(url) - - def html(self, content): - self.request_handler.write(content) - - def session_get(self, name, default=None): - value = self.request_handler.get_secure_cookie(name) - if value: - return json.loads(value.decode()) - return default - - def session_set(self, name, value): - self.request_handler.set_secure_cookie(name, json.dumps(value).encode()) - - def session_pop(self, name): - value = self.session_get(name) - self.request_handler.clear_cookie(name) - return value - - def session_setdefault(self, name, value): - pass - - def build_absolute_uri(self, path=None): - return build_absolute_uri('{0}://{1}'.format(self.request.protocol, - self.request.host), - path) - - def partial_to_session(self, next, backend, request=None, *args, **kwargs): - return json.dumps(super(TornadoStrategy, self).partial_to_session( - next, backend, request=request, *args, **kwargs - )) - - def partial_from_session(self, session): - if session: - return super(TornadoStrategy, self).partial_to_session( - json.loads(session) - ) +from social_tornado.strategy import TornadoTemplateStrategy, TornadoStrategy diff --git a/social/strategies/utils.py b/social/strategies/utils.py index 6374c6bf5..5c23071bb 100644 --- a/social/strategies/utils.py +++ b/social/strategies/utils.py @@ -1,26 +1,2 @@ -from social.utils import module_member - - -# Current strategy getter cache, currently only used by Django to set a method -# to get the current strategy which is latter used by backends get_user() -# method to retrieve the user saved in the session. Backends need an strategy -# to properly access the storage, but Django does not know about that when -# creates the backend instance, this method workarounds the problem. -_current_strategy_getter = None - - -def get_strategy(strategy, storage, *args, **kwargs): - Strategy = module_member(strategy) - Storage = module_member(storage) - return Strategy(Storage, *args, **kwargs) - - -def set_current_strategy_getter(func): - global _current_strategy_getter - _current_strategy_getter = func - - -def get_current_strategy(): - global _current_strategy_getter - if _current_strategy_getter is not None: - return _current_strategy_getter() +from social_core.utils import get_strategy, set_current_strategy_getter, \ + get_current_strategy diff --git a/social/strategies/webpy_strategy.py b/social/strategies/webpy_strategy.py index a6ae53824..39f2e19ec 100644 --- a/social/strategies/webpy_strategy.py +++ b/social/strategies/webpy_strategy.py @@ -1,65 +1 @@ -import web - -from social.strategies.base import BaseStrategy, BaseTemplateStrategy - - -class WebpyTemplateStrategy(BaseTemplateStrategy): - def render_template(self, tpl, context): - return web.template.render(tpl)(**context) - - def render_string(self, html, context): - return web.template.Template(html)(**context) - - -class WebpyStrategy(BaseStrategy): - DEFAULT_TEMPLATE_STRATEGY = WebpyTemplateStrategy - - def get_setting(self, name): - return getattr(web.config, name) - - def request_data(self, merge=True): - if merge: - data = web.input(_method='both') - elif web.ctx.method == 'POST': - data = web.input(_method='post') - else: - data = web.input(_method='get') - return data - - def request_host(self): - return web.ctx.host - - def redirect(self, url): - return web.seeother(url) - - def html(self, content): - web.header('Content-Type', 'text/html;charset=UTF-8') - return content - - def render_html(self, tpl=None, html=None, context=None): - if not tpl and not html: - raise ValueError('Missing template or html parameters') - context = context or {} - if tpl: - tpl = web.template.frender(tpl) - else: - tpl = web.template.Template(html) - return tpl(**context) - - def session_get(self, name, default=None): - return web.web_session.get(name, default) - - def session_set(self, name, value): - web.web_session[name] = value - - def session_pop(self, name): - return web.web_session.pop(name, None) - - def session_setdefault(self, name, value): - return web.web_session.setdefault(name, value) - - def build_absolute_uri(self, path=None): - path = path or '' - if path.startswith('http://') or path.startswith('https://'): - return path - return web.ctx.protocol + '://' + web.ctx.host + path +from social_webpy.strategy import WebpyTemplateStrategy, WebpyStrategy diff --git a/social/tests/requirements-pypy.txt b/social/tests/requirements-pypy.txt index 33cef2575..0b01edb8d 100644 --- a/social/tests/requirements-pypy.txt +++ b/social/tests/requirements-pypy.txt @@ -3,6 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 rednose>=0.4.1 -requests>=1.1.0 -PyJWT>=1.0.0,<2.0.0 unittest2==0.5.1 +social-auth-core diff --git a/social/tests/requirements-python3.txt b/social/tests/requirements-python3.txt index bfae69a02..550752706 100644 --- a/social/tests/requirements-python3.txt +++ b/social/tests/requirements-python3.txt @@ -3,6 +3,5 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 rednose>=0.4.1 -requests>=1.1.0 -PyJWT>=1.0.0,<2.0.0 unittest2py3k==0.5.1 +social-auth-core diff --git a/social/tests/requirements.txt b/social/tests/requirements.txt index 6bc042d7b..525eac07a 100644 --- a/social/tests/requirements.txt +++ b/social/tests/requirements.txt @@ -3,7 +3,6 @@ coverage>=3.6 mock==1.0.1 nose>=1.2.1 rednose>=0.4.1 -requests>=1.1.0 -PyJWT>=1.0.0,<2.0.0 unittest2==0.5.1 python-saml==2.1.3 +social-auth-core diff --git a/social/utils.py b/social/utils.py index f40537533..2ad53c798 100644 --- a/social/utils.py +++ b/social/utils.py @@ -1,265 +1,6 @@ -import re -import sys -import unicodedata -import collections -import functools -import logging - -import six -import requests -import social - -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.poolmanager import PoolManager - -from social.exceptions import AuthCanceled, AuthUnreachableProvider -from social.p3 import urlparse, urlunparse, urlencode, \ - parse_qs as battery_parse_qs - - -SETTING_PREFIX = 'SOCIAL_AUTH' - -social_logger = logging.getLogger('social') - - -class SSLHttpAdapter(HTTPAdapter): - """" - Transport adapter that allows to use any SSL protocol. Based on: - http://requests.rtfd.org/latest/user/advanced/#example-specific-ssl-version - """ - def __init__(self, ssl_protocol): - self.ssl_protocol = ssl_protocol - super(SSLHttpAdapter, self).__init__() - - def init_poolmanager(self, connections, maxsize, block=False): - self.poolmanager = PoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - ssl_version=self.ssl_protocol - ) - - @classmethod - def ssl_adapter_session(cls, ssl_protocol): - session = requests.Session() - session.mount('https://', SSLHttpAdapter(ssl_protocol)) - return session - - -def import_module(name): - __import__(name) - return sys.modules[name] - - -def module_member(name): - mod, member = name.rsplit('.', 1) - module = import_module(mod) - return getattr(module, member) - - -def user_agent(): - """Builds a simple User-Agent string to send in requests""" - return 'python-social-auth-' + social.__version__ - - -def url_add_parameters(url, params): - """Adds parameters to URL, parameter will be repeated if already present""" - if params: - fragments = list(urlparse(url)) - value = parse_qs(fragments[4]) - value.update(params) - fragments[4] = urlencode(value) - url = urlunparse(fragments) - return url - - -def to_setting_name(*names): - return '_'.join([name.upper().replace('-', '_') for name in names if name]) - - -def setting_name(*names): - return to_setting_name(*((SETTING_PREFIX,) + names)) - - -def sanitize_redirect(hosts, redirect_to): - """ - Given a list of hostnames and an untrusted URL to redirect to, - this method tests it to make sure it isn't garbage/harmful - and returns it, else returns None, similar as how's it done - on django.contrib.auth.views. - """ - if redirect_to: - try: - # Don't redirect to a host that's not in the list - netloc = urlparse(redirect_to)[1] or hosts[0] - except (TypeError, AttributeError): - pass - else: - if netloc in hosts: - return redirect_to - - -def user_is_authenticated(user): - if user and hasattr(user, 'is_authenticated'): - if isinstance(user.is_authenticated, collections.Callable): - authenticated = user.is_authenticated() - else: - authenticated = user.is_authenticated - elif user: - authenticated = True - else: - authenticated = False - return authenticated - - -def user_is_active(user): - if user and hasattr(user, 'is_active'): - if isinstance(user.is_active, collections.Callable): - is_active = user.is_active() - else: - is_active = user.is_active - elif user: - is_active = True - else: - is_active = False - return is_active - - -# This slugify version was borrowed from django revision a61dbd6 -def slugify(value): - """Converts to lowercase, removes non-word characters (alphanumerics - and underscores) and converts spaces to hyphens. Also strips leading - and trailing whitespace.""" - value = unicodedata.normalize('NFKD', value) \ - .encode('ascii', 'ignore') \ - .decode('ascii') - value = re.sub('[^\w\s-]', '', value).strip().lower() - return re.sub('[-\s]+', '-', value) - - -def first(func, items): - """Return the first item in the list for what func returns True""" - for item in items: - if func(item): - return item - - -def parse_qs(value): - """Like urlparse.parse_qs but transform list values to single items""" - return drop_lists(battery_parse_qs(value)) - - -def drop_lists(value): - out = {} - for key, val in value.items(): - val = val[0] - if isinstance(key, six.binary_type): - key = six.text_type(key, 'utf-8') - if isinstance(val, six.binary_type): - val = six.text_type(val, 'utf-8') - out[key] = val - return out - - -def partial_pipeline_data(backend, user=None, *args, **kwargs): - partial = backend.strategy.session_get('partial_pipeline', None) - if partial: - idx, backend_name, xargs, xkwargs = \ - backend.strategy.partial_from_session(partial) - - partial_matches_request = False - - if backend_name == backend.name: - partial_matches_request = True - - req_data = backend.strategy.request_data() - # Normally when resuming a pipeline, request_data will be empty. We - # only need to check for a uid match if new data was provided (i.e. - # if current request specifies the ID_KEY). - if backend.ID_KEY in req_data: - id_from_partial = xkwargs.get('uid') - id_from_request = req_data.get(backend.ID_KEY) - - if id_from_partial != id_from_request: - partial_matches_request = False - - if partial_matches_request: - kwargs.setdefault('pipeline_index', idx) - if user: # don't update user if it's None - kwargs.setdefault('user', user) - kwargs.setdefault('request', backend.strategy.request_data()) - xkwargs.update(kwargs) - return xargs, xkwargs - else: - backend.strategy.clean_partial_pipeline() - - -def build_absolute_uri(host_url, path=None): - """Build absolute URI with given (optional) path""" - path = path or '' - if path.startswith('http://') or path.startswith('https://'): - return path - if host_url.endswith('/') and path.startswith('/'): - path = path[1:] - return host_url + path - - -def constant_time_compare(val1, val2): - """ - Returns True if the two strings are equal, False otherwise. - The time taken is independent of the number of characters that match. - This code was borrowed from Django 1.5.4-final - """ - if len(val1) != len(val2): - return False - result = 0 - if six.PY3 and isinstance(val1, bytes) and isinstance(val2, bytes): - for x, y in zip(val1, val2): - result |= x ^ y - else: - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - return result == 0 - - -def is_url(value): - return value and \ - (value.startswith('http://') or - value.startswith('https://') or - value.startswith('/')) - - -def setting_url(backend, *names): - for name in names: - if is_url(name): - return name - else: - value = backend.setting(name) - if is_url(value): - return value - - -def handle_http_errors(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except requests.HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(args[0], response=err.response) - elif err.response.status_code == 503: - raise AuthUnreachableProvider(args[0]) - else: - raise - return wrapper - - -def append_slash(url): - """Make sure we append a slash at the end of the URL otherwise we - have issues with urljoin Example: - >>> urlparse.urljoin('http://www.example.com/api/v3', 'user/1/') - 'http://www.example.com/api/user/1/' - """ - if url and not url.endswith('/'): - url = '{0}/'.format(url) - return url +from social_core.utils import social_logger, SSLHttpAdapter, import_module, \ + module_member, user_agent, url_add_parameters, to_setting_name, \ + setting_name, sanitize_redirect, user_is_authenticated, user_is_active, \ + slugify, first, parse_qs, drop_lists, partial_pipeline_data, \ + build_absolute_uri, constant_time_compare, is_url, setting_url, \ + handle_http_errors, append_slash diff --git a/tox.ini b/tox.ini index 3d1952b57..0ddc92f13 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py33, py34, pypy, doc +envlist = py27, py33, py34, py35, pypy [testenv] commands = nosetests --where=social/tests --stop @@ -19,6 +19,9 @@ deps = -r{toxinidir}/social/tests/requirements-python3.txt [testenv:py34] deps = -r{toxinidir}/social/tests/requirements-python3.txt +[testenv:py35] +deps = -r{toxinidir}/social/tests/requirements-python3.txt + [testenv:doc] changedir = docs deps = sphinx From 1e37f5e21d2043ca08683d76ff9576e8c785010f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 3 Dec 2016 11:10:28 -0300 Subject: [PATCH 868/890] Deprecation notice --- README.rst | 324 +---------------------------------------------------- 1 file changed, 6 insertions(+), 318 deletions(-) diff --git a/README.rst b/README.rst index f2b051689..0ec3b88a7 100644 --- a/README.rst +++ b/README.rst @@ -8,323 +8,11 @@ Crafted using base code from django-social-auth, it implements a common interfac to define new authentication providers from third parties, and to bring support for more frameworks and ORMs. -.. image:: https://travis-ci.org/omab/python-social-auth.png?branch=master - :target: https://travis-ci.org/omab/python-social-auth +Deprecation notice - 03-12-2016 +------------------------------- -.. image:: https://badge.fury.io/py/python-social-auth.png - :target: http://badge.fury.io/py/python-social-auth +As for Dec 03 2016, this library is now deprecated, the codebase was +split and migrated into the `python-social-auth organization`_, +where a more organized development process is expected to take place. -.. image:: https://readthedocs.org/projects/python-social-auth/badge/?version=latest - :target: https://readthedocs.org/projects/python-social-auth/?badge=latest - :alt: Documentation Status - -.. contents:: Table of Contents - - -Features -======== - -This application provides user registration and login using social sites -credentials. Here are some features, which is probably not a full list yet. - - -Supported frameworks --------------------- - -Multiple frameworks are supported: - - * Django_ - * Flask_ - * Pyramid_ - * Webpy_ - * Tornado_ - -More frameworks can be added easily (and should be even easier in the future -once the code matures). - - -Auth providers --------------- - -Several services are supported by simply defining backends (new ones can be easily added -or current ones extended): - - * Amazon_ OAuth2 http://login.amazon.com/website - * Angel_ OAuth2 - * AOL_ OpenId http://www.aol.com/ - * Appsfuel_ OAuth2 - * ArcGIS_ OAuth2 - * Behance_ OAuth2 - * BelgiumEIDOpenId_ OpenId https://www.e-contract.be/ - * Bitbucket_ OAuth1 - * Box_ OAuth2 - * Clef_ OAuth2 - * Coursera_ OAuth2 - * Dailymotion_ OAuth2 - * DigitalOcean_ OAuth2 https://developers.digitalocean.com/documentation/oauth/ - * Disqus_ OAuth2 - * Douban_ OAuth1 and OAuth2 - * Dropbox_ OAuth1 and OAuth2 - * Evernote_ OAuth1 - * Exacttarget OAuth2 - * Facebook_ OAuth2 and OAuth2 for Applications - * Fedora_ OpenId http://fedoraproject.org/wiki/OpenID - * Fitbit_ OAuth2 and OAuth1 - * Flickr_ OAuth1 - * Foursquare_ OAuth2 - * `Google App Engine`_ Auth - * Github_ OAuth2 - * Google_ OAuth1, OAuth2 and OpenId - * Instagram_ OAuth2 - * Itembase_ OAuth2 - * Jawbone_ OAuth2 https://jawbone.com/up/developer/authentication - * Kakao_ OAuth2 https://developer.kakao.com - * `Khan Academy`_ OAuth1 - * Launchpad_ OpenId - * Line_ OAuth2 - * Linkedin_ OAuth1 - * Live_ OAuth2 - * Livejournal_ OpenId - * LoginRadius_ OAuth2 and Application Auth - * Mailru_ OAuth2 - * MapMyFitness_ OAuth2 - * Mendeley_ OAuth1 http://mendeley.com - * Mixcloud_ OAuth2 - * `Moves app`_ OAuth2 https://dev.moves-app.com/docs/authentication - * `Mozilla Persona`_ - * NaszaKlasa_ OAuth2 - * `NGPVAN ActionID`_ OpenId - * Odnoklassniki_ OAuth2 and Application Auth - * OpenId_ - * OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth - * OpenSuse_ OpenId http://en.opensuse.org/openSUSE:Connect - * Pinterest_ OAuth2 - * PixelPin_ OAuth2 - * Pocket_ OAuth2 - * Podio_ OAuth2 - * Rdio_ OAuth1 and OAuth2 - * Readability_ OAuth1 - * Reddit_ OAuth2 https://github.com/reddit/reddit/wiki/OAuth2 - * Shopify_ OAuth2 - * Sketchfab_ OAuth2 - * Skyrock_ OAuth1 - * Soundcloud_ OAuth2 - * Stackoverflow_ OAuth2 - * Steam_ OpenId - * Stocktwits_ OAuth2 - * Strava_ OAuth2 - * Stripe_ OAuth2 - * Taobao_ OAuth2 http://open.taobao.com/doc/detail.htm?id=118 - * ThisIsMyJam_ OAuth1 https://www.thisismyjam.com/developers/authentication - * Trello_ OAuth1 https://trello.com/docs/gettingstarted/oauth.html - * Tripit_ OAuth1 - * Tumblr_ OAuth1 - * Twilio_ Auth - * Twitter_ OAuth1 - * Uber_ OAuth2 - * Untappd_ OAuth2 - * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications - * Weibo_ OAuth2 - * Withings_ OAuth1 - * Wunderlist_ OAuth2 - * Xing_ OAuth1 - * Yahoo_ OpenId and OAuth2 - * Yammer_ OAuth2 - * Yandex_ OAuth1, OAuth2 and OpenId - * Zotero_ OAuth1 - - -User data ---------- - -Basic user data population, to allow custom field values from provider's -response. - - -Social accounts association ---------------------------- - -Multiple social accounts can be associated to a single user. - - -Authentication processing -------------------------- - -Extensible pipeline to handle authentication/association mechanism in ways that -suits your project. - - -Dependencies -============ - -Dependencies that **must** be met to use the application: - -- OpenId_ support depends on python-openid_ - -- OAuth_ support depends on requests-oauthlib_ - -- Several backends demand application registration on their corresponding - sites and other dependencies like sqlalchemy_ on Flask and Webpy. - -- Other dependencies: - * six_ - * requests_ - - -Documents -========= - -Project homepage is available at http://psa.matiasaguirre.net/ and documents at -http://psa.matiasaguirre.net or http://python-social-auth.readthedocs.org/. - - -Installation -============ - -From pypi_:: - - $ pip install python-social-auth - -Or:: - - $ easy_install python-social-auth - -Or clone from github_:: - - $ git clone git://github.com/omab/python-social-auth.git - -And add social to ``PYTHONPATH``:: - - $ export PYTHONPATH=$PYTHONPATH:$(pwd)/python-social-auth/ - -Or:: - - $ cd python-social-auth - $ sudo python setup.py install - - -Upgrading ---------- - -Django with South -~~~~~~~~~~~~~~~~~ - -Upgrading from 0.1 to 0.2 is likely to cause problems trying to apply a migration when the tables -already exist. In this case a fake migration needs to be applied:: - - $ python manage.py migrate --fake default - - -Support ---------------------- - -If you're having problems with using the project, use the support forum at CodersClan. - -.. image:: http://www.codersclan.net/graphics/getSupport_github4.png - :target: http://codersclan.net/forum/index.php?repo_id=8 - - -Copyrights and License -====================== - -``python-social-auth`` is protected by BSD license. Check the LICENSE_ for -details. - -The base work was derived from django-social-auth_ work and copyrighted too, -check `django-social-auth LICENSE`_ for details: - -.. _LICENSE: https://github.com/omab/python-social-auth/blob/master/LICENSE -.. _django-social-auth: https://github.com/omab/django-social-auth -.. _django-social-auth LICENSE: https://github.com/omab/django-social-auth/blob/master/LICENSE -.. _OpenId: http://openid.net/ -.. _OAuth: http://oauth.net/ -.. _myOpenID: https://www.myopenid.com/ -.. _Angel: https://angel.co -.. _Appsfuel: http://docs.appsfuel.com -.. _ArcGIS: http://www.arcgis.com/ -.. _Behance: https://www.behance.net -.. _Bitbucket: https://bitbucket.org -.. _Box: https://www.box.com -.. _Clef: https://getclef.com/ -.. _Coursera: https://www.coursera.org/ -.. _Dailymotion: https://dailymotion.com -.. _DigitalOcean: https://www.digitalocean.com/ -.. _Disqus: https://disqus.com -.. _Douban: http://www.douban.com -.. _Dropbox: https://dropbox.com -.. _Evernote: https://www.evernote.com -.. _Facebook: https://www.facebook.com -.. _Fitbit: https://fitbit.com -.. _Flickr: http://www.flickr.com -.. _Foursquare: https://foursquare.com -.. _Google App Engine: https://developers.google.com/appengine/ -.. _Github: https://github.com -.. _Google: http://google.com -.. _Instagram: https://instagram.com -.. _Itembase: https://www.itembase.com -.. _LaunchPad: https://help.launchpad.net/YourAccount/OpenID -.. _Line: https://line.me/ -.. _Linkedin: https://www.linkedin.com -.. _Live: https://live.com -.. _Livejournal: http://livejournal.com -.. _Khan Academy: https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication -.. _Mailru: https://mail.ru -.. _MapMyFitness: http://www.mapmyfitness.com/ -.. _Mixcloud: https://www.mixcloud.com -.. _Moves app: https://dev.moves-app.com/docs/ -.. _Mozilla Persona: http://www.mozilla.org/persona/ -.. _NaszaKlasa: https://developers.nk.pl/ -.. _NGPVAN ActionID: http://developers.ngpvan.com/action-id -.. _Odnoklassniki: http://www.odnoklassniki.ru -.. _Pocket: http://getpocket.com -.. _Podio: https://podio.com -.. _Shopify: http://shopify.com -.. _Sketchfab: https://sketchfab.com/developers/oauth -.. _Skyrock: https://skyrock.com -.. _Soundcloud: https://soundcloud.com -.. _Stocktwits: https://stocktwits.com -.. _Strava: http://strava.com -.. _Stripe: https://stripe.com -.. _Taobao: http://open.taobao.com/doc/detail.htm?id=118 -.. _Tripit: https://www.tripit.com -.. _Twilio: https://www.twilio.com -.. _Twitter: http://twitter.com -.. _Uber: http://uber.com -.. _VK.com: http://vk.com -.. _Weibo: https://weibo.com -.. _Wunderlist: https://wunderlist.com -.. _Xing: https://www.xing.com -.. _Yahoo: http://yahoo.com -.. _Yammer: https://www.yammer.com -.. _Yandex: https://yandex.ru -.. _Readability: http://www.readability.com/ -.. _Stackoverflow: http://stackoverflow.com/ -.. _Steam: http://steamcommunity.com/ -.. _Rdio: https://www.rdio.com -.. _Tumblr: http://www.tumblr.com/ -.. _Amazon: http://login.amazon.com/website -.. _AOL: http://www.aol.com/ -.. _BelgiumEIDOpenId: https://www.e-contract.be/ -.. _Fedora: http://fedoraproject.org/wiki/OpenID -.. _Jawbone: https://jawbone.com/up/developer/authentication -.. _Mendeley: http://mendeley.com -.. _Reddit: https://github.com/reddit/reddit/wiki/OAuth2 -.. _OpenSuse: http://en.opensuse.org/openSUSE:Connect -.. _ThisIsMyJam: https://www.thisismyjam.com/developers/authentication -.. _Trello: https://trello.com/docs/gettingstarted/oauth.html -.. _Django: https://github.com/omab/python-social-auth/tree/master/social/apps/django_app -.. _Flask: https://github.com/omab/python-social-auth/tree/master/social/apps/flask_app -.. _Pyramid: http://www.pylonsproject.org/projects/pyramid/about -.. _Webpy: https://github.com/omab/python-social-auth/tree/master/social/apps/webpy_app -.. _Tornado: http://www.tornadoweb.org/ -.. _python-openid: http://pypi.python.org/pypi/python-openid/ -.. _requests-oauthlib: https://requests-oauthlib.readthedocs.org/ -.. _sqlalchemy: http://www.sqlalchemy.org/ -.. _pypi: http://pypi.python.org/pypi/python-social-auth/ -.. _OpenStreetMap: http://www.openstreetmap.org -.. _six: http://pythonhosted.org/six/ -.. _requests: http://docs.python-requests.org/en/latest/ -.. _PixelPin: http://pixelpin.co.uk -.. _Zotero: http://www.zotero.org/ -.. _Pinterest: https://www.pinterest.com -.. _Untappd: https://untappd.com/ +.. _python-social-auth organization: https://github.com/python-social-auth From de9f179b6dfe3a03cc5e83b21beb00eb8b015f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 3 Dec 2016 11:10:35 -0300 Subject: [PATCH 869/890] v0.3.0 --- CHANGELOG.md | 6 +- Makefile | 18 -- docs/Makefile | 130 --------- docs/backends/amazon.rst | 29 -- docs/backends/angel.rst | 21 -- docs/backends/aol.rst | 11 - docs/backends/appsfuel.rst | 46 ---- docs/backends/arcgis.rst | 25 -- docs/backends/azuread.rst | 20 -- docs/backends/battlenet.rst | 34 --- docs/backends/beats.rst | 25 -- docs/backends/behance.rst | 31 --- docs/backends/belgium_eid.rst | 11 - docs/backends/bitbucket.rst | 56 ---- docs/backends/box.rst | 23 -- docs/backends/changetip.rst | 22 -- docs/backends/clef.rst | 15 - docs/backends/coinbase.rst | 22 -- docs/backends/coursera.rst | 26 -- docs/backends/dailymotion.rst | 23 -- docs/backends/digitalocean.rst | 24 -- docs/backends/disqus.rst | 20 -- docs/backends/docker.rst | 20 -- docs/backends/douban.rst | 47 ---- docs/backends/dribbble.rst | 23 -- docs/backends/drip.rst | 14 - docs/backends/dropbox.rst | 42 --- docs/backends/edmodo.rst | 22 -- docs/backends/email.rst | 58 ---- docs/backends/eveonline.rst | 23 -- docs/backends/evernote.rst | 24 -- docs/backends/facebook.rst | 91 ------- docs/backends/fedora.rst | 11 - docs/backends/fitbit.rst | 42 --- docs/backends/flickr.rst | 19 -- docs/backends/foursquare.rst | 14 - docs/backends/github.rst | 61 ----- docs/backends/github_enterprise.rst | 59 ---- docs/backends/google.rst | 252 ----------------- docs/backends/implementation.rst | 308 --------------------- docs/backends/index.rst | 156 ----------- docs/backends/instagram.rst | 25 -- docs/backends/itembase.rst | 51 ---- docs/backends/jawbone.rst | 22 -- docs/backends/justgiving.rst | 23 -- docs/backends/kakao.rst | 17 -- docs/backends/khanacademy.rst | 25 -- docs/backends/lastfm.rst | 17 -- docs/backends/launchpad.rst | 11 - docs/backends/line.rst | 7 - docs/backends/linkedin.rst | 68 ----- docs/backends/live.rst | 24 -- docs/backends/livejournal.rst | 16 -- docs/backends/loginradius.rst | 48 ---- docs/backends/mailru.rst | 7 - docs/backends/mapmyfitness.rst | 13 - docs/backends/meetup.rst | 14 - docs/backends/mendeley.rst | 38 --- docs/backends/mineid.rst | 25 -- docs/backends/mixcloud.rst | 31 --- docs/backends/moves.rst | 31 --- docs/backends/naszaklasa.rst | 26 -- docs/backends/nationbuilder.rst | 30 -- docs/backends/naver.rst | 27 -- docs/backends/ngpvan_actionid.rst | 36 --- docs/backends/oauth.rst | 31 --- docs/backends/odnoklassnikiru.rst | 56 ---- docs/backends/openid.rst | 46 ---- docs/backends/openstreetmap.rst | 21 -- docs/backends/orbi.rst | 17 -- docs/backends/persona.rst | 43 --- docs/backends/pinterest.rst | 29 -- docs/backends/pixelpin.rst | 33 --- docs/backends/pocket.rst | 12 - docs/backends/podio.rst | 13 - docs/backends/qiita.rst | 23 -- docs/backends/qq.rst | 32 --- docs/backends/rdio.rst | 46 ---- docs/backends/readability.rst | 24 -- docs/backends/reddit.rst | 33 --- docs/backends/runkeeper.rst | 13 - docs/backends/salesforce.rst | 44 --- docs/backends/saml.rst | 169 ------------ docs/backends/shopify.rst | 29 -- docs/backends/sketchfab.rst | 17 -- docs/backends/skyrock.rst | 21 -- docs/backends/slack.rst | 23 -- docs/backends/soundcloud.rst | 25 -- docs/backends/spotify.rst | 25 -- docs/backends/stackoverflow.rst | 19 -- docs/backends/steam.rst | 19 -- docs/backends/stocktwits.rst | 16 -- docs/backends/strava.rst | 17 -- docs/backends/stripe.rst | 34 --- docs/backends/suse.rst | 13 - docs/backends/taobao.rst | 15 - docs/backends/thisismyjam.rst | 17 -- docs/backends/trello.rst | 26 -- docs/backends/tripit.rst | 16 -- docs/backends/tumblr.rst | 16 -- docs/backends/twilio.rst | 22 -- docs/backends/twitch.rst | 19 -- docs/backends/twitter.rst | 34 --- docs/backends/uber.rst | 28 -- docs/backends/untappd.rst | 28 -- docs/backends/upwork.rst | 28 -- docs/backends/username.rst | 52 ---- docs/backends/vend.rst | 24 -- docs/backends/vimeo.rst | 28 -- docs/backends/vk.rst | 131 --------- docs/backends/weibo.rst | 23 -- docs/backends/withings.rst | 13 - docs/backends/wunderlist.rst | 13 - docs/backends/xing.rst | 14 - docs/backends/yahoo.rst | 33 --- docs/backends/yammer.rst | 30 -- docs/backends/zotero.rst | 25 -- docs/conf.py | 22 -- docs/configuration/cherrypy.rst | 81 ------ docs/configuration/django.rst | 213 --------------- docs/configuration/flask.rst | 160 ----------- docs/configuration/index.rst | 24 -- docs/configuration/porting_from_dsa.rst | 145 ---------- docs/configuration/pyramid.rst | 137 ---------- docs/configuration/settings.rst | 311 --------------------- docs/configuration/webpy.rst | 74 ----- docs/copyright.rst | 12 - docs/developer_intro.rst | 170 ------------ docs/exceptions.rst | 55 ---- docs/index.rst | 46 ---- docs/installing.rst | 58 ---- docs/intro.rst | 183 ------------- docs/logging_out.rst | 25 -- docs/pipeline.rst | 348 ------------------------ docs/storage.rst | 200 -------------- docs/strategies.rst | 77 ------ docs/tests.rst | 48 ---- docs/thanks.rst | 219 --------------- docs/use_cases.rst | 312 --------------------- requirements-python3.txt | 19 -- site/css/bootstrap-responsive.min.css | 9 - site/css/bootstrap.min.css | 9 - site/css/site.css | 4 - site/docs | 1 - site/img/glyphicons-halflings-white.png | Bin 8777 -> 0 bytes site/img/glyphicons-halflings.png | Bin 12799 -> 0 bytes site/index.html | 104 ------- site/js/bootstrap.min.js | 6 - social/__init__.py | 2 +- social/apps/flask_app/peewee/models.py | 2 +- tox.ini | 5 - 151 files changed, 7 insertions(+), 7208 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/backends/amazon.rst delete mode 100644 docs/backends/angel.rst delete mode 100644 docs/backends/aol.rst delete mode 100644 docs/backends/appsfuel.rst delete mode 100644 docs/backends/arcgis.rst delete mode 100644 docs/backends/azuread.rst delete mode 100644 docs/backends/battlenet.rst delete mode 100644 docs/backends/beats.rst delete mode 100644 docs/backends/behance.rst delete mode 100644 docs/backends/belgium_eid.rst delete mode 100644 docs/backends/bitbucket.rst delete mode 100644 docs/backends/box.rst delete mode 100644 docs/backends/changetip.rst delete mode 100644 docs/backends/clef.rst delete mode 100644 docs/backends/coinbase.rst delete mode 100644 docs/backends/coursera.rst delete mode 100644 docs/backends/dailymotion.rst delete mode 100644 docs/backends/digitalocean.rst delete mode 100644 docs/backends/disqus.rst delete mode 100644 docs/backends/docker.rst delete mode 100644 docs/backends/douban.rst delete mode 100644 docs/backends/dribbble.rst delete mode 100644 docs/backends/drip.rst delete mode 100644 docs/backends/dropbox.rst delete mode 100644 docs/backends/edmodo.rst delete mode 100644 docs/backends/email.rst delete mode 100644 docs/backends/eveonline.rst delete mode 100644 docs/backends/evernote.rst delete mode 100644 docs/backends/facebook.rst delete mode 100644 docs/backends/fedora.rst delete mode 100644 docs/backends/fitbit.rst delete mode 100644 docs/backends/flickr.rst delete mode 100644 docs/backends/foursquare.rst delete mode 100644 docs/backends/github.rst delete mode 100644 docs/backends/github_enterprise.rst delete mode 100644 docs/backends/google.rst delete mode 100644 docs/backends/implementation.rst delete mode 100644 docs/backends/index.rst delete mode 100644 docs/backends/instagram.rst delete mode 100644 docs/backends/itembase.rst delete mode 100644 docs/backends/jawbone.rst delete mode 100644 docs/backends/justgiving.rst delete mode 100644 docs/backends/kakao.rst delete mode 100644 docs/backends/khanacademy.rst delete mode 100644 docs/backends/lastfm.rst delete mode 100644 docs/backends/launchpad.rst delete mode 100644 docs/backends/line.rst delete mode 100644 docs/backends/linkedin.rst delete mode 100644 docs/backends/live.rst delete mode 100644 docs/backends/livejournal.rst delete mode 100644 docs/backends/loginradius.rst delete mode 100644 docs/backends/mailru.rst delete mode 100644 docs/backends/mapmyfitness.rst delete mode 100644 docs/backends/meetup.rst delete mode 100644 docs/backends/mendeley.rst delete mode 100644 docs/backends/mineid.rst delete mode 100644 docs/backends/mixcloud.rst delete mode 100644 docs/backends/moves.rst delete mode 100644 docs/backends/naszaklasa.rst delete mode 100644 docs/backends/nationbuilder.rst delete mode 100644 docs/backends/naver.rst delete mode 100644 docs/backends/ngpvan_actionid.rst delete mode 100644 docs/backends/oauth.rst delete mode 100644 docs/backends/odnoklassnikiru.rst delete mode 100644 docs/backends/openid.rst delete mode 100644 docs/backends/openstreetmap.rst delete mode 100644 docs/backends/orbi.rst delete mode 100644 docs/backends/persona.rst delete mode 100644 docs/backends/pinterest.rst delete mode 100644 docs/backends/pixelpin.rst delete mode 100644 docs/backends/pocket.rst delete mode 100644 docs/backends/podio.rst delete mode 100644 docs/backends/qiita.rst delete mode 100644 docs/backends/qq.rst delete mode 100644 docs/backends/rdio.rst delete mode 100644 docs/backends/readability.rst delete mode 100644 docs/backends/reddit.rst delete mode 100644 docs/backends/runkeeper.rst delete mode 100644 docs/backends/salesforce.rst delete mode 100644 docs/backends/saml.rst delete mode 100644 docs/backends/shopify.rst delete mode 100644 docs/backends/sketchfab.rst delete mode 100644 docs/backends/skyrock.rst delete mode 100644 docs/backends/slack.rst delete mode 100644 docs/backends/soundcloud.rst delete mode 100644 docs/backends/spotify.rst delete mode 100644 docs/backends/stackoverflow.rst delete mode 100644 docs/backends/steam.rst delete mode 100644 docs/backends/stocktwits.rst delete mode 100644 docs/backends/strava.rst delete mode 100644 docs/backends/stripe.rst delete mode 100644 docs/backends/suse.rst delete mode 100644 docs/backends/taobao.rst delete mode 100644 docs/backends/thisismyjam.rst delete mode 100644 docs/backends/trello.rst delete mode 100644 docs/backends/tripit.rst delete mode 100644 docs/backends/tumblr.rst delete mode 100644 docs/backends/twilio.rst delete mode 100644 docs/backends/twitch.rst delete mode 100644 docs/backends/twitter.rst delete mode 100644 docs/backends/uber.rst delete mode 100644 docs/backends/untappd.rst delete mode 100644 docs/backends/upwork.rst delete mode 100644 docs/backends/username.rst delete mode 100644 docs/backends/vend.rst delete mode 100644 docs/backends/vimeo.rst delete mode 100644 docs/backends/vk.rst delete mode 100644 docs/backends/weibo.rst delete mode 100644 docs/backends/withings.rst delete mode 100644 docs/backends/wunderlist.rst delete mode 100644 docs/backends/xing.rst delete mode 100644 docs/backends/yahoo.rst delete mode 100644 docs/backends/yammer.rst delete mode 100644 docs/backends/zotero.rst delete mode 100644 docs/conf.py delete mode 100644 docs/configuration/cherrypy.rst delete mode 100644 docs/configuration/django.rst delete mode 100644 docs/configuration/flask.rst delete mode 100644 docs/configuration/index.rst delete mode 100644 docs/configuration/porting_from_dsa.rst delete mode 100644 docs/configuration/pyramid.rst delete mode 100644 docs/configuration/settings.rst delete mode 100644 docs/configuration/webpy.rst delete mode 100644 docs/copyright.rst delete mode 100644 docs/developer_intro.rst delete mode 100644 docs/exceptions.rst delete mode 100644 docs/index.rst delete mode 100644 docs/installing.rst delete mode 100644 docs/intro.rst delete mode 100644 docs/logging_out.rst delete mode 100644 docs/pipeline.rst delete mode 100644 docs/storage.rst delete mode 100644 docs/strategies.rst delete mode 100644 docs/tests.rst delete mode 100644 docs/thanks.rst delete mode 100644 docs/use_cases.rst delete mode 100644 site/css/bootstrap-responsive.min.css delete mode 100644 site/css/bootstrap.min.css delete mode 100644 site/css/site.css delete mode 120000 site/docs delete mode 100644 site/img/glyphicons-halflings-white.png delete mode 100644 site/img/glyphicons-halflings.png delete mode 100644 site/index.html delete mode 100644 site/js/bootstrap.min.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c16a9aab0..213820206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## [Unreleased](https://github.com/omab/python-social-auth/tree/HEAD) -[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.21...HEAD) +[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.3.0...HEAD) + +## [v0.3.0](https://github.com/omab/python-social-auth/tree/v0.3.0) (2016-12-03) + +Deprecated in favor of [python-social-auth organization](https://github.com/python-social-auth) ## [v0.2.21](https://github.com/omab/python-social-auth/tree/v0.2.21) (2016-08-15) diff --git a/Makefile b/Makefile index 7b91f4d55..264ec46d8 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,3 @@ -docs: - sphinx-build docs/ docs/_build/ - -site: docs - rsync -avkz site/ tarf:sites/psa/ - build: python setup.py sdist python setup.py bdist_wheel --python-tag py2 @@ -14,9 +8,6 @@ publish: python setup.py bdist_wheel --python-tag py2 upload BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3 upload -run-tox: - @ tox - docker-tox-build: @ docker build -t omab/psa-legacy . @@ -26,16 +17,7 @@ docker-tox: docker-tox-build -v "`pwd`:/code" \ -w /code omab/psa-legacy tox -docker-shell: docker-tox-build - @ docker run -it --rm \ - --name psa-legacy-test \ - -v "`pwd`:/code" \ - -w /code omab/psa-legacy bash - clean: @ find . -name '*.py[co]' -delete @ find . -name '__pycache__' -delete @ rm -rf *.egg-info dist build - - -.PHONY: site docs publish diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 525f59e5e..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,130 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoSocialAuth.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoSocialAuth.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoSocialAuth" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoSocialAuth" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/backends/amazon.rst b/docs/backends/amazon.rst deleted file mode 100644 index 72ccd0a95..000000000 --- a/docs/backends/amazon.rst +++ /dev/null @@ -1,29 +0,0 @@ -Amazon -====== - -Amazon implemented OAuth2 protocol for their authentication mechanism. To -enable ``python-social-auth`` support follow this steps: - -1. Go to `Amazon App Console`_ and create an application. - -2. Fill App Id and Secret in your project settings:: - - SOCIAL_AUTH_AMAZON_KEY = '...' - SOCIAL_AUTH_AMAZON_SECRET = '...' - -3. Enable the backend:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.amazon.AmazonOAuth2', - ... - ) - -Further documentation at `Website Developer Guide`_ and `Getting Started for Web`_. - -**Note:** This backend supports TLSv1 protocol since SSL will be deprecated - from May 25, 2015 - -.. _Amazon App Console: http://login.amazon.com/manageApps -.. _Website Developer Guide: https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf -.. _Getting Started for Web: http://login.amazon.com/website diff --git a/docs/backends/angel.rst b/docs/backends/angel.rst deleted file mode 100644 index 76f243a78..000000000 --- a/docs/backends/angel.rst +++ /dev/null @@ -1,21 +0,0 @@ -Angel List -========== - -Angel uses OAuth v2 for Authentication. - -- Register a new application at the `Angel List API`_, and - -- fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_ANGEL_KEY = '' - SOCIAL_AUTH_ANGEL_SECRET = '' - -- extra scopes can be defined by using:: - - SOCIAL_AUTH_ANGEL_AUTH_EXTRA_ARGUMENTS = {'scope': 'email messages'} - -**Note:** -Angel List does not currently support returning ``state`` parameter used to -validate the auth process. - -.. _Angel List API: https://angel.co/api/oauth/faq diff --git a/docs/backends/aol.rst b/docs/backends/aol.rst deleted file mode 100644 index 86893c54e..000000000 --- a/docs/backends/aol.rst +++ /dev/null @@ -1,11 +0,0 @@ -AOL -=== - -AOL OpenId doesn't require major settings beside being defined on -``AUTHENTICATION_BACKENDS```:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.aol.AOLOpenId', - ... - ) diff --git a/docs/backends/appsfuel.rst b/docs/backends/appsfuel.rst deleted file mode 100644 index 79957fc32..000000000 --- a/docs/backends/appsfuel.rst +++ /dev/null @@ -1,46 +0,0 @@ -Appsfuel -======== - -Appsfuel uses OAuth v2 for Authentication check the `official docs`_ too. - -- Sign up at the `Appsfuel Developer Program`_ - -- Create and verify a new app - -- On the dashboard click on **Show API keys** - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_APPSFUEL_KEY = '' - SOCIAL_AUTH_APPSFUEL_SECRET = '' - -Appsfuel gives you the chance to integrate with **Live** or **Sandbox** env. - - -Appsfuel Live -------------- - -- Add 'social.backends.contrib.appsfuel.AppsfuelBackend' into your - ``AUTHENTICATION_BACKENDS``. - -- Then you can start using ``{% url social:begin 'appsfuel' %}`` in your - templates - - -Appsfuel Sandbox ----------------- - -- Add ``'social.backends.appsfuel.AppsfuelOAuth2Sandbox'`` into your - ``AUTHENTICATION_BACKENDS``. - -- Then you can start using ``{% url social:begin 'appsfuel-sandbox' %}`` in - your templates - -- Define the settings:: - - SOCIAL_AUTH_APPSFUEL_SANDBOX_KEY = '' - SOCIAL_AUTH_APPSFUEL_SANDBOX_SECRET = '' - - -.. _official docs: http://docs.appsfuel.com/api_reference#api_integration -.. _Appsfuel Developer Program: https://developer.appsfuel.com diff --git a/docs/backends/arcgis.rst b/docs/backends/arcgis.rst deleted file mode 100644 index b32578138..000000000 --- a/docs/backends/arcgis.rst +++ /dev/null @@ -1,25 +0,0 @@ -ArcGIS -====== - -ArcGIS uses OAuth2 for authentication. - -- Register a new application at `ArcGIS Developer Center`_. - - -OAuth2 ------- - -1. Add the OAuth2 backend to your settings page:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.arcgis.ArcGISOAuth2', - ... - ) - -2. Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_ARCGIS_KEY = '' - SOCIAL_AUTH_ARCGIS_SECRET = '' - -.. _ArcGIS Developer Center: https://developers.arcgis.com/ diff --git a/docs/backends/azuread.rst b/docs/backends/azuread.rst deleted file mode 100644 index 6c84c5cc3..000000000 --- a/docs/backends/azuread.rst +++ /dev/null @@ -1,20 +0,0 @@ -Microsoft Azure Active Directory -================================ - -To enable OAuth2 support: - -- Fill in ``Client ID`` and ``Client Secret`` settings. These values can be - obtained easily as described in `Azure AD Application Registration`_ doc:: - - SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '' - SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_AZUREAD_OAUTH2_RESOURCE = '' - - This is the resource you would like to access after authentication succeeds. - Some of the possible values are: ``https://graph.windows.net`` or - ``https://-my.sharepoint.com``. - -.. _Azure AD Application Registration: https://msdn.microsoft.com/en-us/library/azure/dn132599.aspx diff --git a/docs/backends/battlenet.rst b/docs/backends/battlenet.rst deleted file mode 100644 index db6a842ee..000000000 --- a/docs/backends/battlenet.rst +++ /dev/null @@ -1,34 +0,0 @@ -Battle.net -========== - -Blizzard implemented OAuth2 protocol for their authentication mechanism. To -enable ``python-social-auth`` support follow this steps: - -1. Go to `Battlenet Developer Portal`_ and create an application. - -2. Fill App Id and Secret in your project settings:: - - SOCIAL_AUTH_BATTLENET_OAUTH2_KEY = '...' - SOCIAL_AUTH_BATTLENET_OAUTH2_SECRET = '...' - -3. Enable the backend:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.battlenet.BattleNetOAuth2', - ... - ) - -Note: If you want to allow the user to choose a username from his own -characters, some further steps are required, see the use cases part of the -documentation. To get the account id and battletag use the user_data function, as -`account id is no longer passed inherently`_. - -Another note: If you get a 500 response "Internal Server Error" the API now requires `https on callback endpoints`_. - -Further documentation at `Developer Guide`_. - -.. _Battlenet Developer Portal: https://dev.battle.net/ -.. _Developer Guide: https://dev.battle.net/docs/read/oauth -.. _https on callback endpoints: http://us.battle.net/en/forum/topic/17085510584 -.. _account id is no longer passed inherently: http://us.battle.net/en/forum/topic/18300183303 diff --git a/docs/backends/beats.rst b/docs/backends/beats.rst deleted file mode 100644 index 27fece0dc..000000000 --- a/docs/backends/beats.rst +++ /dev/null @@ -1,25 +0,0 @@ -Beats -===== - -Beats supports OAuth 2. - -- Register a new application at `Beats Music API`_, and follow the - instructions below. - -OAuth2 ------- - -Add the Beats OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.beats.BeatsOAuth2', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_BEATS_OAUTH2_KEY = '' - SOCIAL_AUTH_BEATS_OAUTH2_SECRET = '' - -.. _Beats Music API: https://developer.beatsmusic.com/docs diff --git a/docs/backends/behance.rst b/docs/backends/behance.rst deleted file mode 100644 index 0ce8fe832..000000000 --- a/docs/backends/behance.rst +++ /dev/null @@ -1,31 +0,0 @@ -Behance -======= - -DEPRECATED NOTICE ------------------ - -**NOTE:** IT SEEMS THAT BEHANCE HAS DROPPED THEIR OAUTH2 SUPPORT WITHOUT MUCH -NOTICE BESIDE A `BLOG POST`_ ON SEPTEMBER 2014 MENTIONING THAT IT WILL BE -INTRODUCED "SOON". THIS BACKEND IS IN DEPRECATED STATE FOR NOW. - -Behance uses OAuth2 for its auth mechanism. - -- Register a new application at `Behance App Registration`_, set your - application name, website and redirect URI. - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_BEHANCE_KEY = '' - SOCIAL_AUTH_BEHANCE_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_BEHANCE_SCOPE = [...] - -Check available permissions at `Possible Scopes`_. Also check the rest of their -doc at `Behance Developer Documentation`_. - -.. _Behance App Registration: http://www.behance.net/dev/register -.. _Possible Scopes: http://www.behance.net/dev/authentication#scopes -.. _Behance Developer Documentation: http://www.behance.net/dev -.. _BLOG POST: http://blog.behance.net/dev/introducing-the-behance-api diff --git a/docs/backends/belgium_eid.rst b/docs/backends/belgium_eid.rst deleted file mode 100644 index a62aca5bb..000000000 --- a/docs/backends/belgium_eid.rst +++ /dev/null @@ -1,11 +0,0 @@ -Belgium EID -=========== - -Belgium EID OpenId doesn't require major settings beside being defined on -``AUTHENTICATION_BACKENDS```:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.belgiumeid.BelgiumEIDOpenId', - ... - ) diff --git a/docs/backends/bitbucket.rst b/docs/backends/bitbucket.rst deleted file mode 100644 index 1cb69dd8e..000000000 --- a/docs/backends/bitbucket.rst +++ /dev/null @@ -1,56 +0,0 @@ -Bitbucket -========= - -Bitbucket supports both OAuth2 and OAuth1 logins. - -1. Register a new OAuth Consumer by following the instructions in the - Bitbucket documentation: `OAuth on Bitbucket`_ - - Note: For OAuth2, your consumer MUST have the "account" scope otherwise - the user profile information (username, name, etc.) won't be accessible. - -2. Configure the appropriate settings for OAuth2 or OAuth1 (see below). - - -OAuth2 ------- - -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - - SOCIAL_AUTH_BITBUCKET_OAUTH2_KEY = '' - SOCIAL_AUTH_BITBUCKET_OAUTH2_SECRET = '' - -- If you would like to restrict access to only users with verified e-mail - addresses, set ``SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY = True`` - By default the setting is set to ``False`` since it's possible for a - project to gather this information by other methods. - - -OAuth1 ------- - -- OAuth1 works similarly to OAuth2, but you must fill in the following settings - instead:: - - SOCIAL_AUTH_BITBUCKET_KEY = '' - SOCIAL_AUTH_BITBUCKET_SECRET = '' - -- If you would like to restrict access to only users with verified e-mail - addresses, set ``SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY = True``. - By default the setting is set to ``False`` since it's possible for a - project to gather this information by other methods. - - -User ID -------- - -Bitbucket recommends the use of UUID_ as the user identifier instead -of ``username`` since they can change and impose a security risk. For -that reason ``UUID`` is used by default, but for backward -compatibility reasons, it's possible to get the old behavior again by -defining this setting:: - - SOCIAL_AUTH_BITBUCKET_USERNAME_AS_ID = True - -.. _UUID: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs -.. _OAuth on Bitbucket: https://confluence.atlassian.com/display/BITBUCKET/OAuth+on+Bitbucket diff --git a/docs/backends/box.rst b/docs/backends/box.rst deleted file mode 100644 index d22d2d409..000000000 --- a/docs/backends/box.rst +++ /dev/null @@ -1,23 +0,0 @@ -Box.net -======= - -Box works similar to Facebook (OAuth2). - -- Register an application at `Manage Box Applications`_ - -- Fill the **Consumer Key** and **Consumer Secret** values in your settings:: - - SOCIAL_AUTH_BOX_KEY = '' - SOCIAL_AUTH_BOX_SECRET = '' - -- By default the token is not permanent, it will last an hour. To refresh the - access token just do:: - - from social.apps.django_app.utils import load_strategy - - strategy = load_strategy(backend='box') - user = User.objects.get(pk=foo) - social = user.social_auth.filter(provider='box')[0] - social.refresh_token(strategy=strategy) - -.. _Manage Box Applications: https://app.box.com/developers/services diff --git a/docs/backends/changetip.rst b/docs/backends/changetip.rst deleted file mode 100644 index 44f97c28b..000000000 --- a/docs/backends/changetip.rst +++ /dev/null @@ -1,22 +0,0 @@ -ChangeTip -========= - -ChangeTip - -- Register a new application at ChangeTip_, set the callback URL to - ``http://example.com/complete/changetip/`` replacing ``example.com`` with your - domain. - -- Fill ``Client ID`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_CHANGETIP_KEY = '' - SOCIAL_AUTH_CHANGETIP_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_CHANGETIP_SCOPE = [...] - - See auth scopes at `ChangeTip OAuth docs`_. - -.. _ChangeTip: https://www.changetip.com/api -.. _ChangeTip OAuth docs: https://www.changetip.com/api/auth/#!#scopes diff --git a/docs/backends/clef.rst b/docs/backends/clef.rst deleted file mode 100644 index cfdc6393a..000000000 --- a/docs/backends/clef.rst +++ /dev/null @@ -1,15 +0,0 @@ -Clef -====== - -Clef works similar to Facebook (OAuth). - -- Register a new application at `Clef Developers`_, set the callback URL to - ``http://example.com/complete/clef/`` replacing ``example.com`` with your - domain. - -- Fill ``App Id`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_CLEF_KEY = '' - SOCIAL_AUTH_CLEF_SECRET = '' - -.. _Clef Developers: https://getclef.com/developer \ No newline at end of file diff --git a/docs/backends/coinbase.rst b/docs/backends/coinbase.rst deleted file mode 100644 index b90cdf336..000000000 --- a/docs/backends/coinbase.rst +++ /dev/null @@ -1,22 +0,0 @@ -Coinbase -======== - -Coinbase uses OAuth2. - -- Register an application at Coinbase_ - -- Fill in the **Client Id** and **Client Secret** values in your settings:: - - SOCIAL_AUTH_COINBASE_KEY = '' - SOCIAL_AUTH_COINBASE_SECRET = '' - -- Set the ``redirect_url`` on coinbase. Make sure to include the trailing - slash, eg. ``http://hostname/complete/coinbase/`` - -- Specify scopes with:: - - SOCIAL_AUTH_COINBASE_SCOPE = [...] - - By default the scope is set to ``balance``. - -.. _Coinbase: https://coinbase.com/oauth/applications diff --git a/docs/backends/coursera.rst b/docs/backends/coursera.rst deleted file mode 100644 index 5e23a7a3c..000000000 --- a/docs/backends/coursera.rst +++ /dev/null @@ -1,26 +0,0 @@ -Coursera -============ - -Coursera uses a variant of OAuth2 authentication. The details of the API -can be found at `OAuth2-based APIs - Coursera Technology`_. - -Take the following steps in order to use the backend: - -1. Create an account at `Coursera`_. - -2. Open `Developer Console`_, create an organisation and application. - -3. Set **Client ID** as a ``SOCIAL_AUTH_COURSERA_KEY`` and -**Secret Key** as a ``SOCIAL_AUTH_COURSERA_SECRET`` in your local settings. - -4. Add the backend to ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.coursera.CourseraOAuth2', - ... - ) - -.. _OAuth2-based APIs - Coursera Technology: https://tech.coursera.org/app-platform/oauth2/ -.. _Coursera: https://accounts.coursera.org/console -.. _Developer Console: https://accounts.coursera.org/console diff --git a/docs/backends/dailymotion.rst b/docs/backends/dailymotion.rst deleted file mode 100644 index c8198c456..000000000 --- a/docs/backends/dailymotion.rst +++ /dev/null @@ -1,23 +0,0 @@ -DailyMotion -=========== - -DailyMotion uses OAuth2. In order to enable the backend follow: - -- Register an application at `DailyMotion Developer Portal`_ - -- Fill in the **Client Id** and **Client Secret** values in your settings:: - - SOCIAL_AUTH_DAILYMOTION_KEY = '' - SOCIAL_AUTH_DAILYMOTION_SECRET = '' - -- Set the ``Callback URL`` to ``http:///complete/dailymotion/`` - -- Specify scopes with:: - - SOCIAL_AUTH_DAILYMOTION_SCOPE = [...] - - Available scopes are listed in the `Requesting Extended Permissions`_ - section. - -.. _DailyMotion Developer Portal: http://www.dailymotion.com/profile/developer/new -.. _Requesting Extended Permissions: http://www.dailymotion.com/doc/api/authentication.html#requesting-extended-permissions diff --git a/docs/backends/digitalocean.rst b/docs/backends/digitalocean.rst deleted file mode 100644 index 5f72cfe02..000000000 --- a/docs/backends/digitalocean.rst +++ /dev/null @@ -1,24 +0,0 @@ -DigitalOcean -============ - -DigitalOcean uses OAuth2 for its auth process. See the full `DigitalOcean -developer's documentation`_ for more information. - -- Register a new application in the `Apps & API page`_ in the DigitalOcean - control panel, setting the callback URL to ``http://example.com/complete/digitalocean/`` - replacing ``example.com`` with your domain. - -- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings:: - - SOCIAL_AUTH_DIGITALOCEAN_KEY = '' - SOCIAL_AUTH_DIGITALOCEAN_SECRET = '' - -- By default, only ``read`` permissions are granted. In order to create, - destroy, and take other actions on the user's resources, you must request - ``read write`` permissions like so:: - - SOCIAL_AUTH_DIGITALOCEAN_AUTH_EXTRA_ARGUMENTS = {'scope': 'read write'} - - -.. _DigitalOcean developer's documentation: https://developers.digitalocean.com/documentation/ -.. _Apps & API page: https://cloud.digitalocean.com/settings/applications diff --git a/docs/backends/disqus.rst b/docs/backends/disqus.rst deleted file mode 100644 index e7fec760c..000000000 --- a/docs/backends/disqus.rst +++ /dev/null @@ -1,20 +0,0 @@ -Disqus -====== - -Disqus uses OAuth v2 for Authentication. - -- Register a new application at the `Disqus API`_, and - -- fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_DISQUS_KEY = '' - SOCIAL_AUTH_DISQUS_SECRET = '' - -- extra scopes can be defined by using:: - - SOCIAL_AUTH_DISQUS_AUTH_EXTRA_ARGUMENTS = {'scope': 'likes comments relationships'} - - Check `Disqus Auth API`_ for details. - -.. _Disqus Auth API: http://disqus.com/api/docs/auth/ -.. _Disqus API: http://disqus.com/api/applications/ diff --git a/docs/backends/docker.rst b/docs/backends/docker.rst deleted file mode 100644 index 7e104d777..000000000 --- a/docs/backends/docker.rst +++ /dev/null @@ -1,20 +0,0 @@ -Docker -====== - -Docker.io OAuth2 ----------------- - -Docker.io now supports OAuth2 for their API. In order to set it up: - -- Register a new application by following the instructions in their website: - `Register Your Application`_ - -- Fill **Consumer Key** and **Consumer Secret** values in settings:: - - SOCIAL_AUTH_DOCKER_KEY = '' - SOCIAL_AUTH_DOCKER_SECRET = '' - -- Add ``'social.backends.docker.DockerOAuth2'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - -.. _Register Your Application: http://docs.docker.io/en/latest/reference/api/docker_io_oauth_api/#register-your-application diff --git a/docs/backends/douban.rst b/docs/backends/douban.rst deleted file mode 100644 index d75d9a2fc..000000000 --- a/docs/backends/douban.rst +++ /dev/null @@ -1,47 +0,0 @@ -Douban -====== - -Douban supports OAuth 1 and 2. - -Douban OAuth1 -------------- - -Douban OAuth 1 works similar to Twitter OAuth. - -Douban offers per application keys named ``Consumer Key`` and ``Consumer -Secret``. To enable Douban OAuth these two keys are needed. Further -documentation at `Douban Services & API`_: - -- Register a new application at `Douban API Key`_, make sure to mark the **web - application** checkbox. - -- Fill **Consumer Key** and **Consumer Secret** values in settings:: - - SOCIAL_AUTH_DOUBAN_KEY = '' - SOCIAL_AUTH_DOUBAN_SECRET = '' - -- Add ``'social.backends.douban.DoubanOAuth'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - - -Douban OAuth2 -------------- - -Recently Douban launched their OAuth2 support and the new developer site, you -can find documentation at `Douban Developers`_. To setup OAuth2 follow: - -- Register a new application at `Create A Douban App`_, make sure to mark the - **web application** checkbox. - -- Fill **Consumer Key** and **Consumer Secret** values in settings:: - - SOCIAL_AUTH_DOUBAN_OAUTH2_KEY = '' - SOCIAL_AUTH_DOUBAN_OAUTH2_SECRET = '' - -- Add ``'social.backends.douban.DoubanOAuth2'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - -.. _Douban Services & API: http://www.douban.com/service/ -.. _Douban API Key: http://www.douban.com/service/apikey/apply -.. _Douban Developers: http://developers.douban.com/ -.. _Create A Douban App : http://developers.douban.com/apikey/apply diff --git a/docs/backends/dribbble.rst b/docs/backends/dribbble.rst deleted file mode 100644 index 64333e9f3..000000000 --- a/docs/backends/dribbble.rst +++ /dev/null @@ -1,23 +0,0 @@ -Dribbble -======== - -Dribbble - -- Register a new application at Dribbble_, set the callback URL - to ``http://example.com/complete/dribbble/`` replacing - ``example.com`` with your domain. - -- Fill ``Client ID`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_DRIBBBLE_KEY = '' - SOCIAL_AUTH_DRIBBBLE_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_DRIBBBLE_SCOPE = [...] - - See auth scopes at `Dribbble Developer docs`_. - - -.. _Dribbble: https://dribbble.com/account/applications/new -.. _Dribbble Developer docs: http://developer.dribbble.com/v1/oauth/ diff --git a/docs/backends/drip.rst b/docs/backends/drip.rst deleted file mode 100644 index 88f09a2ed..000000000 --- a/docs/backends/drip.rst +++ /dev/null @@ -1,14 +0,0 @@ -Drip -==== - -Drip uses OAuth v2 for Authentication. - -- Register a new application with `Drip`_, and - -- fill ``Client ID`` and ``Client Secret`` from getdrip.com values in - the settings:: - - SOCIAL_AUTH_DRIP_KEY = '' - SOCIAL_AUTH_DRIP_SECRET = '' - -.. _Drip: https://www.getdrip.com/user/applications diff --git a/docs/backends/dropbox.rst b/docs/backends/dropbox.rst deleted file mode 100644 index a8aa62b78..000000000 --- a/docs/backends/dropbox.rst +++ /dev/null @@ -1,42 +0,0 @@ -Dropbox -======= - -Dropbox supports both OAuth 1 and 2. - -- Register a new application at `Dropbox Developers`_, and follow the - instructions below for the version of OAuth for which you are adding - support. - -OAuth1 ------- - -Add the Dropbox OAuth backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.dropbox.DropboxOAuth', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_DROPBOX_KEY = '' - SOCIAL_AUTH_DROPBOX_SECRET = '' - -OAuth2 ------- - -Add the Dropbox OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.dropbox.DropboxOAuth2', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_DROPBOX_OAUTH2_KEY = '' - SOCIAL_AUTH_DROPBOX_OAUTH2_SECRET = '' - -.. _Dropbox Developers: https://www.dropbox.com/developers/apps diff --git a/docs/backends/edmodo.rst b/docs/backends/edmodo.rst deleted file mode 100644 index 095c45627..000000000 --- a/docs/backends/edmodo.rst +++ /dev/null @@ -1,22 +0,0 @@ -Edmodo -====== - -Edmodo supports OAuth 2. - -- Register a new application at `Edmodo Connect API`_, and follow the - instructions below. -- Add the Edmodo OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.edmodo.EdmodoOAuth2', - ... - ) - -- Fill ``App Key``, ``App Secret`` and ``App Scope`` values in the settings:: - - SOCIAL_AUTH_EDMODO_OAUTH2_KEY = '' - SOCIAL_AUTH_EDMODO_OAUTH2_SECRET = '' - SOCIAL_AUTH_EDMODO_SCOPE = ['basic'] - -.. _Edmodo Connect API: https://developers.edmodo.com/edmodo-connect/edmodo-connect-overview-getting-started/ diff --git a/docs/backends/email.rst b/docs/backends/email.rst deleted file mode 100644 index 9e11b3d6c..000000000 --- a/docs/backends/email.rst +++ /dev/null @@ -1,58 +0,0 @@ -Email Auth -========== - -python-social-auth_ comes with an EmailAuth_ backend which comes handy when -your site uses requires the plain old email and password authentication -mechanism. - -Actually that's a lie since the backend doesn't handle password at all, that's -up to the developer to validate the password in and the proper place to do it -is the pipeline, right after the user instance was retrieved or created. - -The reason to leave password handling to the developer is because too many -things are really tied to the project, like the field where the password is -stored, salt handling, password hashing algorithm and validation. So just add -the pipeline functions that will do that following the needs of your project. - - -Backend settings ----------------- - -``SOCIAL_AUTH_EMAIL_FORM_URL = '/login-form/'`` - Used to redirect the user to the login/signup form, it must have at least - one field named ``email``. Form submit should go to ``/complete/email``, - or if it goes to your view, then your view should complete the process - calling ``social.actions.do_complete``. - -``SOCIAL_AUTH_EMAIL_FORM_HTML = 'login_form.html'`` - The template will be used to render the login/signup form to the user, it - must have at least one field named ``email``. Form submit should go to - ``/complete/email``, or if it goes to your view, then your view should - complete the process calling ``social.actions.do_complete``. - - -Email validation ----------------- - -Check *Email validation* pipeline in the `pipeline docs`_. - -Password handling ------------------ - -Here's an example of password handling to add to the pipeline:: - - def user_password(strategy, backend, user, is_new=False, *args, **kwargs): - if backend.name != 'email': - return - - password = strategy.request_data()['password'] - if is_new: - user.set_password(password) - user.save() - elif not user.validate_password(password): - # return {'user': None, 'social': None} - raise AuthForbidden(backend) - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _EmailAuth: https://github.com/omab/python-social-auth/blob/master/social/backends/email.py#L5 -.. _pipeline docs: ../pipeline.html#email-validation diff --git a/docs/backends/eveonline.rst b/docs/backends/eveonline.rst deleted file mode 100644 index 7c0503746..000000000 --- a/docs/backends/eveonline.rst +++ /dev/null @@ -1,23 +0,0 @@ -EVE Online Single Sign-On (SSO) -=============================== - -The EVE Single Sign-On (SSO) works similar to GitHub (OAuth2). - -- Register a new application at `EVE Developers`_, set the callback URL to - ``http://example.com/complete/eveonline/`` replacing ``example.com`` with your - domain. - -- Fill the ``Client ID`` and ``Secret Key`` values from EVE Developers in the settings:: - - SOCIAL_AUTH_EVEONLINE_KEY = '' - SOCIAL_AUTH_EVEONLINE_SECRET = '' - -- If you want to use EVE Character names as user names, use this setting:: - - SOCIAL_AUTH_CLEAN_USERNAMES = False - -- If you want to access EVE Online's CREST API, use:: - - SOCIAL_AUTH_EVEONLINE_SCOPE = ['publicData'] - -.. _EVE Developers: https://developers.eveonline.com/ diff --git a/docs/backends/evernote.rst b/docs/backends/evernote.rst deleted file mode 100644 index a432326b6..000000000 --- a/docs/backends/evernote.rst +++ /dev/null @@ -1,24 +0,0 @@ -Evernote OAuth -============== - -Evernote OAuth 1.0 for its authentication workflow. - -- Register a new application at `Evernote API Key form`_. - -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - - SOCIAL_AUTH_EVERNOTE_KEY = '' - SOCIAL_AUTH_EVERNOTE_SECRET = '' - - -Sandbox -------- - -Evernote supports a sandbox mode for testing, there's a custom backend for it -which name is ``evernote-sandbox`` instead of ``evernote``. Same settings apply -but use these instead:: - - SOCIAL_AUTH_EVERNOTE_SANDBOX_KEY = '' - SOCIAL_AUTH_EVERNOTE_SANDBOX_SECRET = '' - -.. _Evernote API Key form: http://dev.evernote.com/support/api_key.php diff --git a/docs/backends/facebook.rst b/docs/backends/facebook.rst deleted file mode 100644 index 894070b3c..000000000 --- a/docs/backends/facebook.rst +++ /dev/null @@ -1,91 +0,0 @@ -Facebook -======== - -OAuth2 ------- - -Facebook uses OAuth2 for its auth process. Further documentation at `Facebook -development resources`_: - -- Register a new application at `Facebook App Creation`_, don't use - ``localhost`` as ``App Domains`` and ``Site URL`` since Facebook won't allow - them. Use a placeholder like ``myapp.com`` and define that domain in your - ``/etc/hosts`` or similar file. - -- fill ``App Id`` and ``App Secret`` values in values:: - - SOCIAL_AUTH_FACEBOOK_KEY = '' - SOCIAL_AUTH_FACEBOOK_SECRET = '' - -- Define ``SOCIAL_AUTH_FACEBOOK_SCOPE`` to get extra permissions - from facebook. Email is not sent by default, to get it, you must request the - ``email`` permission:: - - SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] - -- Define ``SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS`` to pass extra parameters - to https://graph.facebook.com/me when gathering the user profile data (you need - to explicitly ask for fields like ``email`` using ``fields`` key):: - - SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { - 'locale': 'ru_RU', - 'fields': 'id, name, email, age_range' - } - -If you define a redirect URL in Facebook setup page, be sure to not define -http://127.0.0.1:8000 or http://localhost:8000 because it won't work when -testing. Instead I define http://myapp.com and setup a mapping on ``/etc/hosts``. - - -Canvas Application ------------------- - -If you need to perform authentication from Facebook Canvas application: - -- Create your canvas application at http://developers.facebook.com/apps - -- In Facebook application settings specify your canvas URL ``mysite.com/fb`` - (current default) - -- Setup your Python Social Auth settings and your application namespace:: - - SOCIAL_AUTH_FACEBOOK_APP_KEY = '' - SOCIAL_AUTH_FACEBOOK_APP_SECRET = '' - SOCIAL_AUTH_FACEBOOK_APP_NAMESPACE = '' - -- Launch your testing server on port 80 (use sudo or nginx or apache) for - browser to be able to load it when Facebook calls canvas URL - -- Open your Facebook page via http://apps.facebook.com/app_namespace or - better via http://www.facebook.com/pages/user-name/user-id?sk=app_app-id - -- After that you will see this page in a right way and will able to connect - to application and login automatically after connection - -- Provide a template to be rendered, it must have this JavaScript snippet (or - similar) in it:: - - - - -More info on the topic at `Facebook Canvas Application Authentication`_. - - -Graph 2.0 ---------- - -If looking for `Graph 2.0`_ support, use the backends ``Facebook2OAuth2`` -(OAuth2) and/or ``Facebook2AppOAuth2`` (Canvas application). - -.. _Facebook development resources: http://developers.facebook.com/docs/authentication/ -.. _Facebook App Creation: http://developers.facebook.com/setup/ -.. _Facebook Canvas Application Authentication: http://www.ikrvss.ru/2011/09/22/django-social-auth-and-facebook-canvas-applications/ -.. _Graph 2.0: https://developers.facebook.com/blog/post/2014/04/30/the-new-facebook-login/ diff --git a/docs/backends/fedora.rst b/docs/backends/fedora.rst deleted file mode 100644 index eb6486111..000000000 --- a/docs/backends/fedora.rst +++ /dev/null @@ -1,11 +0,0 @@ -Fedora -====== - -Fedora OpenId doesn't require major settings beside being defined on -``AUTHENTICATION_BACKENDS```:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.fedora.FedoraOpenId', - ... - ) diff --git a/docs/backends/fitbit.rst b/docs/backends/fitbit.rst deleted file mode 100644 index 057a84c85..000000000 --- a/docs/backends/fitbit.rst +++ /dev/null @@ -1,42 +0,0 @@ -Fitbit -====== - -Fitbit supports both OAuth 2.0 and OAuth 1.0a logins. OAuth 2 is -preferred for new integrations, as OAuth 1.0a does not support getting -heartrate or location and will be deprecated in the future. - -1. Register a new OAuth Consumer `here`_ - -2. Configure the appropriate settings for OAuth 2.0 or OAuth 1.0a (see - below). - -OAuth 2.0 or OAuth 1.0a ------------------------ - -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the - settings:: - - SOCIAL_AUTH_FITBIT_KEY = '' - SOCIAL_AUTH_FITBIT_SECRET = '' - -OAuth 2.0 specific settings ---------------------------- - -By default, only the ``profile`` scope is requested. To request more -scopes, set SOCIAL_AUTH_FITBIT_SCOPE:: - - SOCIAL_AUTH_FITBIT_SCOPE = [ - 'activity', - 'heartrate', - 'location', - 'nutrition', - 'profile', - 'settings', - 'sleep', - 'social', - 'weight' - ] - -The above will request all permissions from the user. - -.. _here: https://dev.fitbit.com/apps/new diff --git a/docs/backends/flickr.rst b/docs/backends/flickr.rst deleted file mode 100644 index 43f8859da..000000000 --- a/docs/backends/flickr.rst +++ /dev/null @@ -1,19 +0,0 @@ -Flickr -====== - -Flickr uses OAuth v1.0 for authentication. - -- Register a new application at the `Flickr App Garden`_, and - -- fill ``Key`` and ``Secret`` values in the settings:: - - SOCIAL_AUTH_FLICKR_KEY = '' - SOCIAL_AUTH_FLICKR_SECRET = '' - -- Flickr might show a messages saying "Oops! Flickr doesn't recognise the - permission set.", if encountered with this error, just define this setting:: - - SOCIAL_AUTH_FLICKR_AUTH_EXTRA_ARGUMENTS = {'perms': 'read'} - - -.. _Flickr App Garden: http://www.flickr.com/services/apps/create/ diff --git a/docs/backends/foursquare.rst b/docs/backends/foursquare.rst deleted file mode 100644 index 9dacca2f4..000000000 --- a/docs/backends/foursquare.rst +++ /dev/null @@ -1,14 +0,0 @@ -Foursquare -========== - -Foursquare uses OAuth2. In order to enable the backend follow: - -- Register an application at `Foursquare Developers Portal`_, - set the ``Redirect URI`` to ``http:///complete/foursquare/`` - -- Fill in the **Client Id** and **Client Secret** values in your settings:: - - SOCIAL_AUTH_FOURSQUARE_KEY = '' - SOCIAL_AUTH_FOURSQUARE_SECRET = '' - -.. _Foursquare Developers Portal: https://foursquare.com/developers/register diff --git a/docs/backends/github.rst b/docs/backends/github.rst deleted file mode 100644 index d96427568..000000000 --- a/docs/backends/github.rst +++ /dev/null @@ -1,61 +0,0 @@ -GitHub -====== - -GitHub works similar to Facebook (OAuth). - -- Register a new application at `GitHub Developers`_, set the callback URL to - ``http://example.com/complete/github/`` replacing ``example.com`` with your - domain. - -- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings:: - - SOCIAL_AUTH_GITHUB_KEY = '' - SOCIAL_AUTH_GITHUB_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_GITHUB_SCOPE = [...] - - -GitHub for Organizations ------------------------- - -When defining authentication for organizations, use the -``GithubOrganizationOAuth2`` backend instead. The settings are the same as -the non-organization backend, but the names must be:: - - SOCIAL_AUTH_GITHUB_ORG_* - -Be sure to define the organization name using the setting:: - - SOCIAL_AUTH_GITHUB_ORG_NAME = '' - -This name will be used to check that the user really belongs to the given -organization and discard it if they're not part of it. - - -GitHub for Teams ----------------- - -Similar to ``GitHub for Organizations``, there's a GitHub for Teams backend, -use the backend ``GithubTeamOAuth2``. The settings are the same as -the basic backend, but the names must be:: - - SOCIAL_AUTH_GITHUB_TEAM_* - -Be sure to define the ``Team ID`` using the setting:: - - SOCIAL_AUTH_GITHUB_TEAM_ID = '' - -This ``id`` will be used to check that the user really belongs to the given -team and discard it if they're not part of it. - - -Github for Enterprises ----------------------- - -Check the docs :ref:`github-enterprise` if planning to use Github -Enterprises. - - -.. _GitHub Developers: https://github.com/settings/applications/new diff --git a/docs/backends/github_enterprise.rst b/docs/backends/github_enterprise.rst deleted file mode 100644 index 29f5d6d83..000000000 --- a/docs/backends/github_enterprise.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. _github-enterprise: - -GitHub Enterprise -================= - -GitHub Enterprise works similar to regular Github, which is in turn based on Facebook (OAuth). - -- Register a new application on your instance of `GitHub Enterprise Developers`_, - set the callback URL to ``http://example.com/complete/github/`` replacing ``example.com`` - with your domain. - -- Set the API URL for your Github Enterprise appliance: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL = 'https://git.example.com/api/v3/' - -- Fill the ``Client ID`` and ``Client Secret`` values from GitHub in the settings: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY = '' - SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_SCOPE = [...] - - -GitHub Enterprise for Organizations ------------------------------------ - -When defining authentication for organizations, use the -``GithubEnterpriseOrganizationOAuth2`` backend instead. The settings are the same as -the non-organization backend, but the names must be:: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_* - -Be sure to define the organization name using the setting:: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME = '' - -This name will be used to check that the user really belongs to the given -organization and discard it if they're not part of it. - - -GitHub Enterprise for Teams ---------------------------- - -Similar to ``GitHub Enterprise for Organizations``, there's a GitHub for Teams backend, -use the backend ``GithubEnterpriseTeamOAuth2``. The settings are the same as -the basic backend, but the names must be:: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_* - -Be sure to define the ``Team ID`` using the setting:: - - SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID = '' - -This ``id`` will be used to check that the user really belongs to the given -team and discard it if they're not part of it. - -.. _GitHub Enterprise Developers: https:///settings/applications/new diff --git a/docs/backends/google.rst b/docs/backends/google.rst deleted file mode 100644 index e252d8895..000000000 --- a/docs/backends/google.rst +++ /dev/null @@ -1,252 +0,0 @@ -Google -====== - -This section describes how to setup the different services provided by Google. - -Google OAuth ------------- - -.. attention:: **Google OAuth deprecation** - Important: OAuth 1.0 was officially deprecated on April 20, 2012, and will be - shut down on April 20, 2015. We encourage you to migrate to any of the other - protocols. - -Google provides ``Consumer Key`` and ``Consumer Secret`` keys to registered -applications, but also allows unregistered application to use their authorization -system with, but beware that this method will display a security banner to the -user telling that the application is not trusted. - -Check `Google OAuth`_ and make your choice. - -- fill ``Consumer Key`` and ``Consumer Secret`` values:: - - SOCIAL_AUTH_GOOGLE_OAUTH_KEY = '' - SOCIAL_AUTH_GOOGLE_OAUTH_SECRET = '' - -anonymous values will be used if not configured as described in their -`OAuth reference`_ - -- setup any needed extra scope in:: - - SOCIAL_AUTH_GOOGLE_OAUTH_SCOPE = [...] - - -Google OAuth2 -------------- - -Recently Google launched OAuth2 support following the definition at `OAuth2 draft`. -It works in a similar way to plain OAuth mechanism, but developers **must** register -an application and apply for a set of keys. Check `Google OAuth2`_ document for details. - -When creating the application in the Google Console be sure to fill the -``PRODUCT NAME`` at ``API & auth -> Consent screen`` form. - -To enable OAuth2 support: - -- fill ``Client ID`` and ``Client Secret`` settings, these values can be obtained - easily as described on `OAuth2 Registering`_ doc:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '' - SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '' - -- setup any needed extra scope:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [...] - -Check which applications can be included in their `Google Data Protocol Directory`_ - - -Google+ Sign-In ---------------- - -`Google+ Sign In`_ works a lot like OAuth2, but most of the initial work is -done by their Javascript which thens calls a defined handler to complete the -auth process. - -* To enable the backend create an application using the `Google - console`_ and following the steps from the `official guide`_. Make - sure to enable the Google+ API in the console. - -* Fill in the key settings looking inside the Google console the subsection - ``Credentials`` inside ``API & auth``:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.google.GooglePlusAuth', - ) - - SOCIAL_AUTH_GOOGLE_PLUS_KEY = '...' - SOCIAL_AUTH_GOOGLE_PLUS_SECRET = '...' - - ``SOCIAL_AUTH_GOOGLE_PLUS_KEY`` corresponds to the variable ``CLIENT ID``. - ``SOCIAL_AUTH_GOOGLE_PLUS_SECRET`` corresponds to the variable - ``CLIENT SECRET``. - -* Add the sign-in button to your template, you can use the SDK button - or add your own and attach the click handler to it (check `Google+ Identity Sign-In`_ - documentation about it):: - -
          Google+ Sign In
          - -* Add the Javascript snippet in the same template as above:: - - - - - -* Logging out - - Logging-out can be tricky when using the the platform SDK because it - can trigger an automatic sign-in when listening to the user status - change. With the method show above, that won't happen, but if the UI - depends more in the SDK values than the backend, then things can get - out of sync easilly. To prevent this, the user should be logged-out - from Google+ platform too. This can be accomplished by doing:: - - - - -Google OpenId -------------- - -Google OpenId works straightforward, not settings are needed. Domains or emails -whitelists can be applied too, check the whitelists_ settings for details. - - -Orkut ------ - -As of September 30, 2014, Orkut has been `shut down`_. - -User identification -------------------- - -Optional support for static and unique Google Profile ID identifiers instead of -using the e-mail address for account association can be enabled with:: - - SOCIAL_AUTH_GOOGLE_OAUTH_USE_UNIQUE_USER_ID = True - -or:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_USE_UNIQUE_USER_ID = True - -depending on the backends in use. - - -Refresh Tokens --------------- - -To get an OAuth2 refresh token along with the access token, you must pass an extra argument: ``access_type=offline``. -To do this with Google+ sign-in:: - - SOCIAL_AUTH_GOOGLE_PLUS_AUTH_EXTRA_ARGUMENTS = { - 'access_type': 'offline' - } - - -Scopes deprecation ------------------- - -Google is deprecating the full-url scopes from `Sept 1, 2014`_ in favor of -``Google+ API`` and the recently introduced shorter scopes names. But -``python-social-auth`` already introduced the scopes change at e3525187_ which -was released at ``v0.1.24``. - -But, to enable the new scopes the application requires ``Google+ API`` to be -enabled in the `Google console`_ dashboard, the change is quick and quite -simple, but if any developer desires to keep using the old scopes, it's -possible with the following settings:: - - # Google OAuth2 (google-oauth2) - SOCIAL_AUTH_GOOGLE_OAUTH2_IGNORE_DEFAULT_SCOPE = True - SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile' - ] - - # Google+ SignIn (google-plus) - SOCIAL_AUTH_GOOGLE_PLUS_IGNORE_DEFAULT_SCOPE = True - SOCIAL_AUTH_GOOGLE_PLUS_SCOPE = [ - 'https://www.googleapis.com/auth/plus.login', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile' - ] - -To ease the change, the old API and scopes is still supported by the -application, the new values are the default option but if having troubles -supporting them you can default to the old values by defining this setting:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_USE_DEPRECATED_API = True - SOCIAL_AUTH_GOOGLE_PLUS_USE_DEPRECATED_API = True - -.. _Google support: http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105 -.. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html -.. _Google OAuth: http://code.google.com/apis/accounts/docs/OAuth.html -.. _Google OAuth2: http://code.google.com/apis/accounts/docs/OAuth2.html -.. _OAuth2 Registering: http://code.google.com/apis/accounts/docs/OAuth2.html#Registering -.. _OAuth2 draft: http://tools.ietf.org/html/draft-ietf-oauth-v2-10 -.. _OAuth reference: http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth -.. _shut down: https://support.google.com/orkut/?csw=1#Authenticating -.. _Google Data Protocol Directory: http://code.google.com/apis/gdata/docs/directory.html -.. _whitelists: ../configuration/settings.html#whitelists -.. _Google+ Sign In: https://developers.google.com/+/web/signin/ -.. _Google console: https://code.google.com/apis/console -.. _official guide: https://developers.google.com/+/web/signin/#step_1_create_a_client_id_and_client_secret -.. _Sept 1, 2014: https://developers.google.com/+/api/auth-migration#timetable -.. _e3525187: https://github.com/omab/python-social-auth/commit/e35251878a88954cecf8e575eca27c63164b9f67 -.. _Google+ Identity Sign-In: https://developers.google.com/identity/sign-in/web/sign-in diff --git a/docs/backends/implementation.rst b/docs/backends/implementation.rst deleted file mode 100644 index 8687b9446..000000000 --- a/docs/backends/implementation.rst +++ /dev/null @@ -1,308 +0,0 @@ -Adding a new backend -==================== - -Add new backends is quite easy, usually adding just a ``class`` with a couple -settings and methods overrides to retrieve user data from services API. Follow -the details below. - - -Common attributes ------------------ - -First, lets check the common attributes for all backend types. - -``name = ''`` - Any backend needs a name, usually the popular name of the service is used, - like ``facebook``, ``twitter``, etc. It must be unique, otherwise another - backend can take precedence if it's listed before in - ``AUTHENTICATION_BACKENDS`` setting. - -``ID_KEY = None`` - Defines the attribute in the service response that identifies the user as - unique in the service, the value is later stored in the ``uid`` attribute - in the ``UserSocialAuth`` instance. - -``REQUIRES_EMAIL_VALIDATION = False`` - Flags the backend to enforce email validation during the pipeline (if the - corresponding pipeline ``social.pipeline.mail.mail_validation`` was - enabled). - -``EXTRA_DATA = None`` - During the auth process some basic user data is returned by the provider or - retrieved by ``user_data()`` method which usually is used to call some API - on the provider to retrieve it. This data will be stored under - ``UserSocialAuth.extra_data`` attribute, but to make it accessible under - some common names on different providers, this attribute defines a list of - tuples in the form ``(name, alias)`` where ``name`` is the key in the user - data (which should be a ``dict`` instance) and ``alias`` is the name to - store it on ``extra_data``. - - -OAuth ------ - -OAuth1 and OAuth2 provide share some common definitions based on the shared -behavior during the auth process, like a successful API response from -``AUTHORIZATION_URL`` usually returns some basic user data like a user Id. - - -Shared attributes -***************** - -``name`` - This defines the backend name and identifies it during the auth process. - The name is used in the URLs ``/login/`` and - ``/complete/``. - -``ID_KEY = 'id'`` - Default key name where user identification field is defined, it's used on - auth process when some basic user data is returned. This Id is stored in - ``UserSocialAuth.uid`` field, this together the ``UserSocialAuth.provider`` - field is used to unique identify a user association. - -``SCOPE_PARAMETER_NAME = 'scope'`` - Scope argument is used to tell the provider the API endpoints you want to - call later, it's a permissions request granted over the ``access_token`` - later retrieved. Default value is ``scope`` since that's usually the name - used in the URL parameter, but can be overridden if needed. - -``DEFAULT_SCOPE = None`` - Some providers give nothing about the user but some basic data in required - like the user Id or an email address. Default scope attribute is used to - specify a default value for ``scope`` argument to request those extra used - bits. - -``SCOPE_SEPARATOR = ' '`` - The ``scope`` argument is usually a list of permissions to request, the - list is joined used a separator, usually just a blank space, but differ - from provider to provider, override the default value with this attribute - if it differs. - - -OAuth2 -****** - -OAuth2 backends are fairly simple to implement; just a few settings, a method -override and it's mostly ready to go. - -The key points on this backends are: - -``AUTHORIZATION_URL`` - This is the entry point for the authorization mechanism, users must be - redirected to this URL, used on ``auth_url`` method which builds the - redirect address with ``AUTHORIZATION_URL`` plus some arguments - (``client_id``, ``redirect_uri``, ``response_type``, and ``state``). - -``ACCESS_TOKEN_URL`` - Must point to the API endpoint that provides an ``access_token`` needed to - authenticate in users behalf on future API calls. - -``REFRESH_TOKEN_URL`` - Some providers give the option to renew the ``access_token`` since they are - usually limited in time, once that time runs out, the token is invalidated - and cannot be used any more. This attribute should point to that API - endpoint. - -``RESPONSE_TYPE`` - The response type expected on the auth process, default value is ``code`` - as dictated by OAuth2 definition. Override it if default value doesn't fit - the provider implementation. - -``STATE_PARAMETER`` - OAuth2 defines that an ``state`` parameter can be passed in order to - validate the process, it's kind of a CSRF check to avoid man in the middle - attacks. Some don't recognise it or don't return it which will make the - auth process invalid. Set this attribute to ``False`` in that case. - -``REDIRECT_STATE`` - For those providers that don't recognise the ``state`` parameter, the app - can add a ``redirect_state`` argument to the ``redirect_uri`` to mimic it. - Set this value to ``False`` if the provider likes to verify the - ``redirect_uri`` value and this parameter invalidates that check. - - -Example code:: - - from social.backends.oauth import BaseOAuth2 - - class GithubOAuth2(BaseOAuth2): - """Github OAuth authentication backend""" - name = 'github' - AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize' - ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' - SCOPE_SEPARATOR = ',' - EXTRA_DATA = [ - ('id', 'id'), - ('expires', 'expires') - ] - - def get_user_details(self, response): - """Return user details from Github account""" - return {'username': response.get('login'), - 'email': response.get('email') or '', - 'first_name': response.get('name')} - - def user_data(self, access_token, *args, **kwargs): - """Loads user data from service""" - url = 'https://api.github.com/user?' + urlencode({ - 'access_token': access_token - }) - try: - return json.load(self.urlopen(url)) - except ValueError: - return None - - -OAuth1 -****** - -OAuth1 process is a bit more trickier, `Twitter Docs`_ explains it quite well. -Beside the ``AUTHORIZATION_URL`` and ``ACCESS_TOKEN_URL`` attributes, a third -one is needed used when starting the process. - -``REQUEST_TOKEN_URL = ''`` - During the auth process an unauthorized token is needed to start the - process, later this token is exchanged for an ``access_token``. This - setting points to the API endpoint where that unauthorized token can be - retrieved. - -Example code:: - - from xml.dom import minidom - - from social.backends.oauth import ConsumerBasedOAuth - - - class TripItOAuth(ConsumerBasedOAuth): - """TripIt OAuth authentication backend""" - name = 'tripit' - AUTHORIZATION_URL = 'https://www.tripit.com/oauth/authorize' - REQUEST_TOKEN_URL = 'https://api.tripit.com/oauth/request_token' - ACCESS_TOKEN_URL = 'https://api.tripit.com/oauth/access_token' - EXTRA_DATA = [('screen_name', 'screen_name')] - - def get_user_details(self, response): - """Return user details from TripIt account""" - try: - first_name, last_name = response['name'].split(' ', 1) - except ValueError: - first_name = response['name'] - last_name = '' - return {'username': response['screen_name'], - 'email': response['email'], - 'fullname': response['name'], - 'first_name': first_name, - 'last_name': last_name} - - def user_data(self, access_token, *args, **kwargs): - """Return user data provided""" - url = 'https://api.tripit.com/v1/get/profile' - request = self.oauth_request(access_token, url) - content = self.fetch_response(request) - try: - dom = minidom.parseString(content) - except ValueError: - return None - - return { - 'id': dom.getElementsByTagName('Profile')[0].getAttribute('ref'), - 'name': dom.getElementsByTagName( - 'public_display_name')[0].childNodes[0].data, - 'screen_name': dom.getElementsByTagName( - 'screen_name')[0].childNodes[0].data, - 'email': dom.getElementsByTagName( - 'is_primary')[0].parentNode.getElementsByTagName( - 'address')[0].childNodes[0].data, - } - - -OpenId ------- - -OpenId is fair simpler that OAuth since it's used for authentication rather -than authorization (regardless it's used for authorization too). - -A single attribute is usually needed, the authentication URL endpoint. - -``URL = ''`` - OpenId endpoint where to redirect the user. - -Sometimes the URL is user dependant, like in myOpenId_ where the URL is -``https://.myopenid.com``. For those cases where the user must -input it's handle (or full URL). The backend must override the ``openid_url()`` -method to retrieve it and return a full URL to where the user will be -redirected. - -Example code:: - - from social.backends.open_id import OpenIdAuth - from social.exceptions import AuthMissingParameter - - - class LiveJournalOpenId(OpenIdAuth): - """LiveJournal OpenID authentication backend""" - name = 'livejournal' - - def get_user_details(self, response): - """Generate username from identity url""" - values = super(LiveJournalOpenId, self).get_user_details(response) - values['username'] = values.get('username') or \ - urlparse.urlsplit(response.identity_url)\ - .netloc.split('.', 1)[0] - return values - - def openid_url(self): - """Returns LiveJournal authentication URL""" - if not self.data.get('openid_lj_user'): - raise AuthMissingParameter(self, 'openid_lj_user') - return 'http://%s.livejournal.com' % self.data['openid_lj_user'] - - -Auth APIs ---------- - -For others authentication types, a ``BaseAuth`` class is defined to help. Those -custom auth methods must override the ``auth_url()`` and ``auth_complete()`` -methods. - -Example code:: - - from google.appengine.api import users - - from social.backends.base import BaseAuth - from social.exceptions import AuthException - - - class GoogleAppEngineAuth(BaseAuth): - """GoogleAppengine authentication backend""" - name = 'google-appengine' - - def get_user_id(self, details, response): - """Return current user id.""" - user = users.get_current_user() - if user: - return user.user_id() - - def get_user_details(self, response): - """Return user basic information (id and email only).""" - user = users.get_current_user() - return {'username': user.user_id(), - 'email': user.email(), - 'fullname': '', - 'first_name': '', - 'last_name': ''} - - def auth_url(self): - """Build and return complete URL.""" - return users.create_login_url(self.redirect_uri) - - def auth_complete(self, *args, **kwargs): - """Completes login process, must return user instance.""" - if not users.get_current_user(): - raise AuthException('Authentication error') - kwargs.update({'response': '', 'backend': self}) - return self.strategy.authenticate(*args, **kwargs) - - -.. _Twitter Docs: https://dev.twitter.com/docs/auth/implementing-sign-twitter -.. _myOpenId: https://www.myopenid.com/ diff --git a/docs/backends/index.rst b/docs/backends/index.rst deleted file mode 100644 index 37c29bbec..000000000 --- a/docs/backends/index.rst +++ /dev/null @@ -1,156 +0,0 @@ -Backends -======== - -Here's a list and detailed instruction on how to setup the support for each -backend. - -Adding new backend support --------------------------- - -Add new backends is quite easy, usually adding just a ``class`` with a couple -methods overrides to retrieve user data from services API. Follow the details -in the *Implementation* docs. - -.. toctree:: - :maxdepth: 2 - - implementation - - -Supported backends ------------------- - -Here's the list of currently supported backends. - -Non-social backends -******************* - -.. toctree:: - :maxdepth: 2 - - email - username - -Base OAuth and OpenId classes -***************************** - -.. toctree:: - :maxdepth: 2 - - oauth - openid - saml - -Social backends -*************** - -.. toctree:: - :maxdepth: 2 - - amazon - angel - aol - appsfuel - arcgis - azuread - battlenet - beats - behance - belgium_eid - bitbucket - box - changetip - clef - coinbase - coursera - dailymotion - digitalocean - disqus - docker - douban - dribbble - drip - dropbox - edmodo - eveonline - evernote - facebook - fedora - fitbit - flickr - foursquare - github - github_enterprise - google - instagram - itembase - jawbone - justgiving - kakao - khanacademy - lastfm - launchpad - line - linkedin - livejournal - live - loginradius - mailru - mapmyfitness - meetup - mendeley - mineid - mixcloud - moves - naszaklasa - nationbuilder - naver - ngpvan_actionid - odnoklassnikiru - openstreetmap - orbi - persona - pinterest - pixelpin - pocket - podio - qiita - qq - rdio - readability - reddit - runkeeper - salesforce - shopify - sketchfab - skyrock - slack - soundcloud - spotify - suse - stackoverflow - steam - stocktwits - strava - stripe - taobao - thisismyjam - trello - tripit - tumblr - twilio - twitch - twitter - uber - untappd - upwork - vend - vimeo - vk - weibo - withings - wunderlist - xing - yahoo - yammer - zotero diff --git a/docs/backends/instagram.rst b/docs/backends/instagram.rst deleted file mode 100644 index dbaf86e1f..000000000 --- a/docs/backends/instagram.rst +++ /dev/null @@ -1,25 +0,0 @@ -Instagram -========= - -Instagram uses OAuth v2 for Authentication. - -- Register a new application at the `Instagram API`_, and - -- Add instagram backend to ``AUTHENTICATION_SETTINGS``:: - - AUTHENTICATION_SETTINGS = ( - ... - 'social.backends.instagram.InstagramOAuth2', - ... - ) - -- fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_INSTAGRAM_KEY = '' - SOCIAL_AUTH_INSTAGRAM_SECRET = '' - -- extra scopes can be defined by using:: - - SOCIAL_AUTH_INSTAGRAM_AUTH_EXTRA_ARGUMENTS = {'scope': 'likes comments relationships'} - -.. _Instagram API: http://instagr.am/developer/ diff --git a/docs/backends/itembase.rst b/docs/backends/itembase.rst deleted file mode 100644 index 715d1b427..000000000 --- a/docs/backends/itembase.rst +++ /dev/null @@ -1,51 +0,0 @@ -Itembase -========= - -Itembase uses OAuth2 for authentication. - -- Register a new application for the `Itembase API`_, and - -- Add itembase live backend and/or sandbox backend to ``AUTHENTICATION_BACKENDS``:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.itembase.ItembaseOAuth2', - 'social.backends.itembase.ItembaseOAuth2Sandbox', - ... - ) - -- fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_ITEMBASE_KEY = '' - SOCIAL_AUTH_ITEMBASE_SECRET = '' - - SOCIAL_AUTH_ITEMBASE_SANDBOX_KEY = '' - SOCIAL_AUTH_ITEMBASE_SANDBOX_SECRET = '' - - -- extra scopes can be defined by using:: - - SOCIAL_AUTH_ITEMBASE_SCOPE = ['connection.transaction', - 'connection.product', - 'connection.profile', - 'connection.buyer'] - SOCIAL_AUTH_ITEMBASE_SANDBOX_SCOPE = SOCIAL_AUTH_ITEMBASE_SCOPE - -To use data from the extra scopes, you need to do an extra activation step -that is not in the usual OAuth flow. For that you can extend your pipeline and -add a function that sends the user to an activation URL that Itembase provides. -The method to retrieve the activation data is included in the backend:: - - @partial - def activation(strategy, backend, response, *args, **kwargs): - if backend.name.startswith("itembase"): - - if strategy.session_pop('itembase_activation_in_progress'): - strategy.session_set('itembase_activated', True) - - if not strategy.session_get('itembase_activated'): - activation_data = backend.activation_data(response) - strategy.session_set('itembase_activation_in_progress', True) - return HttpResponseRedirect(activation_data['activation_url']) - -.. _Itembase API: http://developers.itembase.com/authentication/index diff --git a/docs/backends/jawbone.rst b/docs/backends/jawbone.rst deleted file mode 100644 index 64d9ec206..000000000 --- a/docs/backends/jawbone.rst +++ /dev/null @@ -1,22 +0,0 @@ -Jawbone -======= - -Jawbone uses OAuth2. In order to enable the backend follow: - -- Register an application at `Jawbone Developer Portal`_, set the ``OAuth - redirect URIs`` to ``http:///complete/jawbone/`` - -- Fill in the **Client Id** and **Client Secret** values in your settings:: - - SOCIAL_AUTH_JAWBONE_KEY = '' - SOCIAL_AUTH_JAWBONE_SECRET = '' - -- Specify scopes with:: - - SOCIAL_AUTH_JAWBONE_SCOPE = [...] - - Available scopes are listed in the `Jawbone Authentication Reference`_, - "socpes" section. - -.. _Jawbone Developer Portal: https://jawbone.com/up/developer/account/ -.. _Jawbone Authentication Reference: https://jawbone.com/up/developer/authentication diff --git a/docs/backends/justgiving.rst b/docs/backends/justgiving.rst deleted file mode 100644 index 9a5fd6ea6..000000000 --- a/docs/backends/justgiving.rst +++ /dev/null @@ -1,23 +0,0 @@ -Just Giving -=========== - -OAuth2 ------- - -Follow the steps at `Just Giving API Docs`_ to register your -application and get the needed keys. - -- Add the Just Giving OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.justgiving.JustGivingOAuth2', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_JUSTGIVING_KEY = '' - SOCIAL_AUTH_JUSTGIVING_SECRET = '' - -.. _Just Giving API Docs: https://api.justgiving.com/docs diff --git a/docs/backends/kakao.rst b/docs/backends/kakao.rst deleted file mode 100644 index 4a2fbaa16..000000000 --- a/docs/backends/kakao.rst +++ /dev/null @@ -1,17 +0,0 @@ -Kakao -====== - -Kakao uses OAuth v2 for Authentication. - -- Register a new applicationat the `Kakao API`_, and - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_KAKAO_KEY = '' - SOCIAL_AUTH_KAKAO_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_KAKAO_SCOPE = [...] - -.. _Kakao API: https://developers.kakao.com/docs/restapi diff --git a/docs/backends/khanacademy.rst b/docs/backends/khanacademy.rst deleted file mode 100644 index 79e2f618c..000000000 --- a/docs/backends/khanacademy.rst +++ /dev/null @@ -1,25 +0,0 @@ -Khan Academy -============ - -Khan Academy uses a variant of OAuth1 authentication flow. Check the API -details at `Khan Academy API Authentication`_. - -Follow this steps in order to use the backend: - -- Register a new application at `Khan Academy API Apps`_, - -- Fill **Consumer Key** and **Consumer Secret** values:: - - SOCIAL_AUTH_KHANACADEMY_OAUTH1_KEY = '' - SOCIAL_AUTH_KHANACADEMY_OAUTH1_SECRET = '' - -- Add the backend to ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.khanacademy.KhanAcademyOAuth1', - ... - ) - -.. _Khan Academy API Authentication: https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication -.. _Khan Academy API Apps: http://www.khanacademy.org/api-apps/register diff --git a/docs/backends/lastfm.rst b/docs/backends/lastfm.rst deleted file mode 100644 index 0dc7e1b30..000000000 --- a/docs/backends/lastfm.rst +++ /dev/null @@ -1,17 +0,0 @@ -Last.fm -======= - -Last.fm uses a similar authentication process than OAuth2 but it's not. In -order to enable the support for it just: - -- Register an application at `Get an API Account`_, set the Last.fm callback to - something sensible like http://your.site/complete/lastfm - -- Fill in the **API Key** and **API Secret** values in your settings:: - - SOCIAL_AUTH_LASTFM_KEY = '' - SOCIAL_AUTH_LASTFM_SECRET = '' - -- Enable the backend in ``AUTHENTICATION_BACKENDS`` setting. - -.. _Get an API Account: http://www.last.fm/api/account/create diff --git a/docs/backends/launchpad.rst b/docs/backends/launchpad.rst deleted file mode 100644 index 7f12f35fe..000000000 --- a/docs/backends/launchpad.rst +++ /dev/null @@ -1,11 +0,0 @@ -Launchpad -========= - -`Ubuntu Launchpad `_ OpenId doesn't require -major settings beside being defined on ``AUTHENTICATION_BACKENDS```:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.launchpad.LaunchpadOpenId', - ... - ) diff --git a/docs/backends/line.rst b/docs/backends/line.rst deleted file mode 100644 index f63c611a1..000000000 --- a/docs/backends/line.rst +++ /dev/null @@ -1,7 +0,0 @@ -Line.me -======= - -Fill App Id and Secret in your project settings:: - - SOCIAL_AUTH_LINE_KEY = '...' - SOCIAL_AUTH_LINE_SECRET = '...' diff --git a/docs/backends/linkedin.rst b/docs/backends/linkedin.rst deleted file mode 100644 index a7f9ccc87..000000000 --- a/docs/backends/linkedin.rst +++ /dev/null @@ -1,68 +0,0 @@ -LinkedIn -======== - -LinkedIn supports OAuth1 and OAuth2. Migration between each type is fair simple -since the same Key / Secret pair is used for both authentication types. - -LinkedIn OAuth setup is similar to any other OAuth service. The auth flow is -explained on `LinkedIn Developers`_ docs. First you will need to register an -app att `LinkedIn Developer Network`_. - - -OAuth1 ------- - -- Fill the application key and secret in your settings:: - - SOCIAL_AUTH_LINKEDIN_KEY = '' - SOCIAL_AUTH_LINKEDIN_SECRET = '' - -- Application scopes can be specified by:: - - SOCIAL_AUTH_LINKEDIN_SCOPE = [...] - - Check the available options at `LinkedIn Scopes`_. If you want to request - a user's email address, you'll need specify that your application needs - access to the email address use the ``r_emailaddress`` scope. - -- To request extra fields using `LinkedIn fields selectors`_ just define this - setting:: - - SOCIAL_AUTH_LINKEDIN_FIELD_SELECTORS = [...] - - with the needed fields selectors, also define ``SOCIAL_AUTH_LINKEDIN_EXTRA_DATA`` - properly as described in `OAuth `_, that way the values will be - stored in ``UserSocialAuth.extra_data`` field. By default ``id``, - ``first-name`` and ``last-name`` are requested and stored. - -For example, to request a user's email, headline, and industry from the -Linkedin API and store the information in ``UserSocialAuth.extra_data``, you -would add these settings:: - - # Add email to requested authorizations. - SOCIAL_AUTH_LINKEDIN_SCOPE = ['r_basicprofile', 'r_emailaddress', ...] - # Add the fields so they will be requested from linkedin. - SOCIAL_AUTH_LINKEDIN_FIELD_SELECTORS = ['email-address', 'headline', 'industry'] - # Arrange to add the fields to UserSocialAuth.extra_data - SOCIAL_AUTH_LINKEDIN_EXTRA_DATA = [('id', 'id'), - ('firstName', 'first_name'), - ('lastName', 'last_name'), - ('emailAddress', 'email_address'), - ('headline', 'headline'), - ('industry', 'industry')] - -OAuth2 ------- - -OAuth2 works exacly the same than OAuth1, but the settings must be named as:: - - SOCIAL_AUTH_LINKEDIN_OAUTH2_* - -Looks like LinkedIn is forcing the definition of the callback URL in the -application when OAuth2 is used. Be sure to set the proper values, otherwise -a ``(400) Client Error: Bad Request`` might be returned by their service. - -.. _LinkedIn fields selectors: http://developer.linkedin.com/docs/DOC-1014 -.. _LinkedIn Scopes: https://developer.linkedin.com/documents/authentication#granting -.. _LinkedIn Developer Network: https://www.linkedin.com/secure/developer -.. _LinkedIn Developers: http://developer.linkedin.com/documents/authentication diff --git a/docs/backends/live.rst b/docs/backends/live.rst deleted file mode 100644 index 0ede6c3af..000000000 --- a/docs/backends/live.rst +++ /dev/null @@ -1,24 +0,0 @@ -MSN Live Connect -================ - -Live uses OAuth2 for its connect workflow, notice that it isn't OAuth WRAP. - -- Register a new application at `Live Connect Developer Center`_, set your site - domain as redirect domain, - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_LIVE_KEY = '' - SOCIAL_AUTH_LIVE_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_LIVE_SCOPE = [...] - - Defaults are ``wl.basic`` and ``wl.emails``. Latter one is necessary to - retrieve user email. - -- Ensure to have a valid ``Redirect URL`` (``http://your-domain/complete/live``) - defined in the application if ``Enhanced security redirection`` is enabled. - -.. _Live Connect Developer Center: https://account.live.com/developers/applications/create diff --git a/docs/backends/livejournal.rst b/docs/backends/livejournal.rst deleted file mode 100644 index 683ef639a..000000000 --- a/docs/backends/livejournal.rst +++ /dev/null @@ -1,16 +0,0 @@ -LiveJournal -=========== - -LiveJournal provides OpenId, it doesn't require any major settings in order to -work, beside being defined on ``AUTHENTICATION_BACKENDS```:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.aol.AOLOpenId', - ... - ) - -LiveJournal OpenId is provided by URLs in the form ``http://.livejournal.com``, -this application retrieves the ``username`` from the data in the current -request by checking a parameter named ``openid_lj_user`` which can be sent by -``POST`` or ``GET``. diff --git a/docs/backends/loginradius.rst b/docs/backends/loginradius.rst deleted file mode 100644 index f16d57689..000000000 --- a/docs/backends/loginradius.rst +++ /dev/null @@ -1,48 +0,0 @@ -LoginRadius -=========== - -LoginRadius uses OAuth2 for Authentication with other providers with an HTML -widget used to trigger the auth process. - -- Register a new application at the `LoginRadius Website`_, and - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_LOGINRADIUS_KEY = '' - SOCIAL_AUTH_LOGINRADIUS_SECRET = '' - -- Since the auth process is triggered by LoginRadius JS script, you need to - sever such content to the user, all you need to do that is a template with - the following content:: - -
          - - - - Put that content in a template named ``loginradius.html`` (accessible to your - framework), or define a name with ``SOCIAL_AUTH_LOGINRADIUS_TEMPLATE`` setting, - like:: - - SOCIAL_AUTH_LOGINRADIUS_LOCAL_HTML = 'loginradius.html' - - The template context will have the current backend instance under the - ``backend`` name, also the application key (``LOGINRADIUS_KEY``) and the - redirect URL (``LOGINRADIUS_REDIRECT_URL``). - -- Further documentation can be found at `LoginRadius API Documentation`_ and - `LoginRadius Datapoints`_ - -.. _LoginRadius Website: https://loginradius.com/ -.. _LoginRadius API Documentation: http://api.loginradius.com/help/ -.. _LoginRadius Datapoints: http://www.loginradius.com/datapoints/ diff --git a/docs/backends/mailru.rst b/docs/backends/mailru.rst deleted file mode 100644 index 9c24dbe60..000000000 --- a/docs/backends/mailru.rst +++ /dev/null @@ -1,7 +0,0 @@ -Mail.ru OAuth -============= - -Mail.ru uses OAuth2 workflow, to use it fill in settings:: - - SOCIAL_AUTH_MAILRU_OAUTH2_KEY = '' - SOCIAL_AUTH_MAILRU_OAUTH2_SECRET = '' diff --git a/docs/backends/mapmyfitness.rst b/docs/backends/mapmyfitness.rst deleted file mode 100644 index 6a5876eb8..000000000 --- a/docs/backends/mapmyfitness.rst +++ /dev/null @@ -1,13 +0,0 @@ -MapMyFitness -============ - -MapMyFitness uses OAuth v2 for authentication. - -- Register a new application at the `MapMyFitness API`_, and - -- fill ``key`` and ``secret`` values in the settings:: - - SOCIAL_AUTH_MAPMYFITNESS_KEY = '' - SOCIAL_AUTH_MAPMYFITNESS_SECRET = '' - -.. _MapMyFitness API: https://www.mapmyapi.com diff --git a/docs/backends/meetup.rst b/docs/backends/meetup.rst deleted file mode 100644 index 558c3ad3c..000000000 --- a/docs/backends/meetup.rst +++ /dev/null @@ -1,14 +0,0 @@ -Meetup -====== - -Meetup.com uses OAuth2 for its auth mechanism. - -- Register a new OAuth Consumer at `Meetup Consumer Registration`_, set your - consumer name, redirect uri. - -- Fill ``key`` and ``secret`` values in the settings:: - - SOCIAL_AUTH_MEETUP_KEY = '' - SOCIAL_AUTH_MEETUP_SECRET = '' - -.. _Meetup Consumer Registration: https://secure.meetup.com/meetup_api/oauth_consumers/create diff --git a/docs/backends/mendeley.rst b/docs/backends/mendeley.rst deleted file mode 100644 index fe2c79da3..000000000 --- a/docs/backends/mendeley.rst +++ /dev/null @@ -1,38 +0,0 @@ -Mendeley -======== - -Mendeley supports OAuth1 and OAuth2, they are in the process of deprecating -OAuth1 API (which should be fully deprecated on April 2014, check their -announcement_). - - -OAuth1 ------- - -In order to support OAuth1 (not recomended, use OAuth2 instead): - -- Register a new application at `Mendeley Application Registration`_ - -- Fill **Consumer Key** and **Consumer Secret** values:: - - SOCIAL_AUTH_MENDELEY_KEY = '' - SOCIAL_AUTH_MENDELEY_SECRET = '' - - -OAuth2 ------- - -In order to support OAuth2: - -- Register a new application at `Mendeley Application Registration`_, or - migrate your OAuth1 application, check their `migration steps here`_. - -- Fill **Application ID** and **Application Secret** values:: - - SOCIAL_AUTH_MENDELEY_OAUTH2_KEY = '' - SOCIAL_AUTH_MENDELEY_OAUTH2_SECRET = '' - - -.. _Mendeley Application Registration: http://dev.mendeley.com/applications/register/ -.. _announcement: https://sites.google.com/site/mendeleyapi/home/authentication -.. _migration steps here: https://groups.google.com/forum/#!topic/mendeley-open-api-developers/KmUQW9I0ST0 diff --git a/docs/backends/mineid.rst b/docs/backends/mineid.rst deleted file mode 100644 index c7b0557c1..000000000 --- a/docs/backends/mineid.rst +++ /dev/null @@ -1,25 +0,0 @@ -MineID -====== - -MineID works similar to Facebook (OAuth). - -- Register a new application at `MineID.org`_, set the callback URL to - ``http://example.com/complete/mineid/`` replacing ``example.com`` with your - domain. - -- Fill ``Client ID`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_MINEID_KEY = '' - SOCIAL_AUTH_MINEID_SECRET = '' - - -Self-hosted MineID ------------------- - -Since MineID is an Open Source software and can be self-hosted, you can -change settings to point to your instance:: - - SOCIAL_AUTH_MINEID_HOST = 'www.your-mineid-instance.com' - SOCIAL_AUTH_MINEID_SCHEME = 'https' # or 'http' - -.. _MineID.org: https://www.mineid.org/ diff --git a/docs/backends/mixcloud.rst b/docs/backends/mixcloud.rst deleted file mode 100644 index 5d3affe3a..000000000 --- a/docs/backends/mixcloud.rst +++ /dev/null @@ -1,31 +0,0 @@ -Mixcloud OAuth2 -=============== - -The `Mixcloud API`_ offers support for authorization. To this backend support: - -- Register a new application at `Mixcloud Developers`_ - -- Add Mixcloud backend to ``AUTHENTICATION_BACKENDS`` in settings:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.mixcloud.MixcloudOAuth2', - ) - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_MIXCLOUD_KEY = '' - SOCIAL_AUTH_MIXCLOUD_SECRET = '' - -- Similar to the other OAuth backends you can define:: - - SOCIAL_AUTH_MIXCLOUD_EXTRA_DATA = [('username', 'username'), - ('name', 'name'), - ('pictures', 'pictures'), - ('url', 'url')] - - as a list of tuples ``(response name, alias)`` to store user profile data on - the ``UserSocialAuth.extra_data``. - -.. _Mixcloud API: http://www.mixcloud.com/developers/documentation -.. _Mixcloud Developers: http://www.mixcloud.com/developers diff --git a/docs/backends/moves.rst b/docs/backends/moves.rst deleted file mode 100644 index d5d7ad924..000000000 --- a/docs/backends/moves.rst +++ /dev/null @@ -1,31 +0,0 @@ -Moves -===== - -Moves_ provides an OAuth2 authentication flow. In order to enable it: - -- Register an application at `Manage Your Apps`_, remember to fill the - ``Redirect URI`` once the application was created. - -- Fill **Client ID** and **Client secret** in the settings:: - - SOCIAL_AUTH_MOVES_KEY = '' - SOCIAL_AUTH_MOVES_SECRET = '' - -- Define the mandatory scope for your application:: - - SOCIAL_AUTH_MOVES_SCOPE = ['activity', 'location'] - - The scope parameter is required by Moves_ but the backend doesn't set - a default one to minimize the application permissions request, so it's - mandatory for the developer to define this setting. - -- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.moves.MovesOAuth2', - ... - ) - -.. _Moves: http://moves-app.com/ -.. _Manage Your Apps: https://dev.moves-app.com/apps diff --git a/docs/backends/naszaklasa.rst b/docs/backends/naszaklasa.rst deleted file mode 100644 index 01fe78e99..000000000 --- a/docs/backends/naszaklasa.rst +++ /dev/null @@ -1,26 +0,0 @@ -NationBuilder -============= - -`NaszaKlasa supports OAuth2`_ as their authentication mechanism. Follow these -steps in order to use it: - -- Register a new application at your `NK Developers`_ (define the `Callback - URL` to ``http://example.com/complete/nk/`` where ``example.com`` - is your domain). - -- Fill the ``Client ID`` and ``Client Secret`` values from the newly created - application:: - - SOCIAL_AUTH_NK_KEY = '' - SOCIAL_AUTH_NK_SECRET = '' - -- Enable the backend in ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.nk.NKOAuth2', - ... - ) - -.. _NaszaKlasa supports OAuth2: https://developers.nk.pl -.. _NK Developers: https://developers.nk.pl/developers/oauth2client/form \ No newline at end of file diff --git a/docs/backends/nationbuilder.rst b/docs/backends/nationbuilder.rst deleted file mode 100644 index a27c3eca2..000000000 --- a/docs/backends/nationbuilder.rst +++ /dev/null @@ -1,30 +0,0 @@ -NationBuilder -============= - -`NationBuilder supports OAuth2`_ as their authentication mechanism. Follow these -steps in order to use it: - -- Register a new application at your `Nation Admin panel`_ (define the `Callback - URL` to ``http://example.com/complete/nationbuilder/`` where ``example.com`` - is your domain). - -- Fill the ``Client ID`` and ``Client Secret`` values from the newly created - application:: - - SOCIAL_AUTH_NATIONBUILDER_KEY = '' - SOCIAL_AUTH_NATIONBUILDER_SECRET = '' - -- Also define your NationBuilder slug:: - - SOCIAL_AUTH_NATIONBUILDER_SLUG = 'your-nationbuilder-slug' - -- Enable the backend in ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.nationbuilder.NationBuilderOAuth2' - ... - ) - -.. _Nation Admin panel: https://psa.nationbuilder.com/admin/apps -.. _NationBuilder supports OAuth2: http://nationbuilder.com/api_quickstart diff --git a/docs/backends/naver.rst b/docs/backends/naver.rst deleted file mode 100644 index 60d21d40f..000000000 --- a/docs/backends/naver.rst +++ /dev/null @@ -1,27 +0,0 @@ -Naver -===== - -Naver uses OAuth v2 for Authentication. - -- Register a new application at the `Naver API`_, and - -- add naver oauth backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.naver.NaverOAuth2', - ... - ) - -- fill ``Client ID`` and ``Client Secret`` from developer.naver.com - values in the settings:: - - SOCIAL_AUTH_NAVER_KEY = '' - SOCIAL_AUTH_NAVER_SECRET = '' - -- you can get EXTRA_DATA:: - - SOCIAL_AUTH_NAVER_EXTRA_DATA = ['nickname', 'gender', 'age', - 'birthday', 'profile_image'] - -.. _Naver API: https://nid.naver.com/devcenter/docs.nhn?menu=API diff --git a/docs/backends/ngpvan_actionid.rst b/docs/backends/ngpvan_actionid.rst deleted file mode 100644 index cc980a70d..000000000 --- a/docs/backends/ngpvan_actionid.rst +++ /dev/null @@ -1,36 +0,0 @@ -NGP VAN ActionID -================ - -`NGP VAN`_'s ActionID_ service provides an OpenID 1.1 endpoint, which provides -first name, last name, email address, and phone number. - -ActionID doesn't require major settings beside being defined on -``AUTHENTICATION_BACKENDS`` - -.. code-block:: python - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.ngpvan.ActionIDOpenID', - ... - ) - - -If you want to be able to access the "phone" attribute offered by NGP VAN -within ``extra_data`` you can add the following to your settings: - -.. code-block:: python - - SOCIAL_AUTH_ACTIONID_OPENID_AX_EXTRA_DATA = [ - ('http://openid.net/schema/contact/phone/business', 'phone') - ] - - -NGP VAN offers the ability to have your domain whitelisted, which will disable -the "{domain} is requesting a link to your ActionID" warning when your app -attempts to login using an ActionID account. Contact -`NGP VAN Developer Support`_ for more information - -.. _NGP VAN: http://www.ngpvan.com/ -.. _ActionID: http://developers.ngpvan.com/action-id -.. _NGP VAN Developer Support: http://developers.ngpvan.com/support/contact diff --git a/docs/backends/oauth.rst b/docs/backends/oauth.rst deleted file mode 100644 index 44b1137f4..000000000 --- a/docs/backends/oauth.rst +++ /dev/null @@ -1,31 +0,0 @@ -OAuth -===== - -OAuth_ communication demands a set of keys exchange to validate the client -authenticity prior to user approbation. Twitter, and Facebook facilitates -these keys by application registration, Google works the same, -but provides the option for unregistered applications. - -Check next sections for details. - -OAuth_ backends also can store extra data in ``UserSocialAuth.extra_data`` -field by defining a set of values names to retrieve from service response. - -Settings is per backend and its name is dynamically checked using uppercase -backend name as prefix:: - - SOCIAL_AUTH__EXTRA_DATA - -Example:: - - SOCIAL_AUTH_FACEBOOK_EXTRA_DATA = [(..., ...)] - -Settings must be a list of tuples mapping value name in response and value -alias used to store. A third value (boolean) is supported, its purpose is -to signal if the value should be discarded if it evaluates to ``False``, this -is to avoid replacing old (needed) values when they don't form part of current -response. If not present, then this check is avoided and the value will replace -any data. - - -.. _OAuth: http://oauth.net/ diff --git a/docs/backends/odnoklassnikiru.rst b/docs/backends/odnoklassnikiru.rst deleted file mode 100644 index 772997566..000000000 --- a/docs/backends/odnoklassnikiru.rst +++ /dev/null @@ -1,56 +0,0 @@ -Odnoklassniki.ru -================ - -There are two options with Odnoklassniki: either you use OAuth2 workflow to -authenticate odnoklassniki users at external site, or you authenticate users -within your IFrame application. - -OAuth2 ------- - -If you use OAuth2 workflow, you need to: - -- register a new application with `OAuth registration form`_ - -- fill out some settings:: - - SOCIAL_AUTH_ODNOKLASSNIKI_OAUTH2_KEY = '' - SOCIAL_AUTH_ODNOKLASSNIKI_OAUTH2_SECRET = '' - SOCIAL_AUTH_ODNOKLASSNIKI_OAUTH2_PUBLIC_NAME = '' - -- add ``'social.backends.odnoklassniki.OdnoklassnikiOAuth2'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - - -IFrame applications -------------------- - -If you want to authenticate users in your IFrame application, - -- read `Rules for application developers`_ - -- fill out `Developers registration form`_ - -- get your personal sandbox - -- fill out some settings:: - - SOCIAL_AUTH_ODNOKLASSNIKI_APP_KEY = '' - SOCIAL_AUTH_ODNOKLASSNIKI_APP_SECRET = '' - SOCIAL_AUTH_ODNOKLASSNIKI_APP_PUBLIC_NAME = '' - -- add ``'social.backends.odnoklassniki.OdnoklassnikiApp'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS`` - -- sign a public offer and do some bureaucracy - -You may also use:: - - SOCIAL_AUTH_ODNOKLASSNIKI_APP_EXTRA_USER_DATA_LIST - -Defaults to empty tuple, for the list of available fields see `Documentation on user.getInfo`_ - -.. _OAuth registration form: https://apiok.ru/wiki/pages/viewpage.action?pageId=42476652 -.. _Rules for application developers: https://apiok.ru/wiki/display/ok/Odnoklassniki.ru+Third+Party+Platform -.. _Developers registration form: https://apiok.ru/wiki/pages/viewpage.action?pageId=5668937 -.. _Documentation on user.getInfo: https://apiok.ru/wiki/display/ok/REST+API+-+users.getInfo diff --git a/docs/backends/openid.rst b/docs/backends/openid.rst deleted file mode 100644 index 0ac806188..000000000 --- a/docs/backends/openid.rst +++ /dev/null @@ -1,46 +0,0 @@ -OpenId -====== - -OpenId_ support is simpler to implement than OAuth_. Google and Yahoo -providers are supported by default, others are supported by POST method -providing endpoint URL. - -OpenId_ backends can store extra data in ``UserSocialAuth.extra_data`` field -by defining a set of values names to retrieve from any of the used schemas, -AttributeExchange and SimpleRegistration. As their keywords differ we need -two settings. - -Settings is per backend, so we have two possible values for each one. Name -is dynamically checked using uppercase backend name as prefix:: - - SOCIAL_AUTH__SREG_EXTRA_DATA - SOCIAL_AUTH__AX_EXTRA_DATA - -Example:: - - SOCIAL_AUTH_GOOGLE_SREG_EXTRA_DATA = [(..., ...)] - SOCIAL_AUTH_GOOGLE_AX_EXTRA_DATA = [(..., ...)] - -Settings must be a list of tuples mapping value name in response and value -alias used to store. A third value (boolean) is supported to, it's purpose is -to signal if the value should be discarded if it evaluates to ``False``, this -is to avoid replacing old (needed) values when they don't form part of current -response. If not present, then this check is avoided and the value will replace -any data. - -Username --------- - -The OpenId_ backend will check for a ``username`` key in the values returned by -the server, but default to ``first-name`` + ``last-name`` if that key is -missing. It's possible to indicate the username key in the values If the -username is under a different key with a setting, but backends should have -defined a default value. For example:: - - SOCIAL_AUTH_FEDORA_USERNAME_KEY = 'nickname' - -This setting indicates that the username should be populated by the -``nickname`` value in the Fedora OpenId_ provider. - -.. _OpenId: http://openid.net/ -.. _OAuth: http://oauth.net/ diff --git a/docs/backends/openstreetmap.rst b/docs/backends/openstreetmap.rst deleted file mode 100644 index b837d8d29..000000000 --- a/docs/backends/openstreetmap.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenStreetMap -============= - -OpenStreetMap supports OAuth 1.0 and 1.0a but 1.0a should be used for the new -applications, as 1.0 is for support of legacy clients only. - -Access tokens currently do not expire automatically. - -More documentation at `OpenStreetMap Wiki`_: - -- Login to your account - -- Register your application as OAuth consumer on your `OpenStreetMap user settings page`_, and - -- Set ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_OPENSTREETMAP_KEY = '' - SOCIAL_AUTH_OPENSTREETMAP_SECRET = '' - -.. _OpenStreetMap Wiki: http://wiki.openstreetmap.org/wiki/OAuth -.. _OpenStreetMap user settings page: http://www.openstreetmap.org/user/username/oauth_clients/new diff --git a/docs/backends/orbi.rst b/docs/backends/orbi.rst deleted file mode 100644 index 0cc0a7904..000000000 --- a/docs/backends/orbi.rst +++ /dev/null @@ -1,17 +0,0 @@ -Orbi -==== - -Orbi OAuth v2 for Authentication. - -- Register a new applicationat the `Orbi API`_, and - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_ORBI_KEY = '' - SOCIAL_AUTH_ORBI_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_KAKAO_SCOPE = ['all'] - -.. _Orbi API: http://orbi.kr diff --git a/docs/backends/persona.rst b/docs/backends/persona.rst deleted file mode 100644 index 0bfd74bce..000000000 --- a/docs/backends/persona.rst +++ /dev/null @@ -1,43 +0,0 @@ -Mozilla Persona -=============== - -Support for `Mozilla Persona`_ is possible by posting the ``assertion`` code to -``/complete/persona/`` URL. - -The setup doesn't need any setting, just the usual `Mozilla Persona`_ -javascript include in your document and the needed mechanism to trigger the -POST to `python-social-auth`_:: - - - - - -
          - - Mozilla Persona -
          - - - - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _Mozilla Persona: http://www.mozilla.org/persona/ diff --git a/docs/backends/pinterest.rst b/docs/backends/pinterest.rst deleted file mode 100644 index 7c18ab004..000000000 --- a/docs/backends/pinterest.rst +++ /dev/null @@ -1,29 +0,0 @@ -Pinterest -========= - -Pinterest implemented OAuth2 protocol for their authentication mechanism. -To enable ``python-social-auth`` support follow this steps: - -1. Go to `Pinterest developers zone`_ and create an application. - -2. Fill App Id and Secret in your project settings:: - - SOCIAL_AUTH_PINTEREST_KEY = '...' - SOCIAL_AUTH_PINTEREST_SECRET = '...' - SOCIAL_AUTH_PINTEREST_SCOPE = [ - 'read_public', - 'write_public', - 'read_relationships', - 'write_relationships' - ] - -3. Enable the backend:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.pinterest.PinterestOAuth2', - ... - ) - -.. _Pinterest developers zone: https://developers.pinterest.com/apps/ -.. _Pinterest Documentation: https://developers.pinterest.com/docs/ diff --git a/docs/backends/pixelpin.rst b/docs/backends/pixelpin.rst deleted file mode 100644 index 8f1ebaf4a..000000000 --- a/docs/backends/pixelpin.rst +++ /dev/null @@ -1,33 +0,0 @@ -PixelPin -======== - -PixelPin only supports OAuth2. - -PixelPin OAuth2 ---------------- - -Developer documentation for PixelPin can be found at -http://developer.pixelpin.co.uk/. To setup OAuth2 do the following: - -- Register a new developer account at `PixelPin Developers`_. - - You require a PixelPin account to create developer accounts. Sign up at - `PixelPin Account Page`_ For the value of redirect uri, use whatever path you - need to return to on your web application. The example code provided with the - plugin uses ``http:///complete/pixelpin-oauth2/``. - - Once verified by email, record the values of client id and secret for the - next step. - -- Fill **Consumer Key** and **Consumer Secret** values in your settings.py - file:: - - SOCIAL_AUTH_PIXELPIN_OAUTH2_KEY = '' - SOCIAL_AUTH_PIXELPIN_OAUTH2_SECRET = '' - -- Add ``'social.backends.pixelpin.PixelPinOAuth2'`` into your - ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - -.. _PixelPin homepage: http://pixelpin.co.uk/ -.. _PixelPin Account Page: https://login.pixelpin.co.uk/ -.. _PixelPin Developers: http://developer.pixelpin.co.uk/ diff --git a/docs/backends/pocket.rst b/docs/backends/pocket.rst deleted file mode 100644 index e70708560..000000000 --- a/docs/backends/pocket.rst +++ /dev/null @@ -1,12 +0,0 @@ -Pocket -====== - -Pocket uses a weird variant of OAuth v2 that only defines a consumer key. - -- Register a new application at the `Pocket API`_, and - -- fill ``consumer key`` value in the settings:: - - SOCIAL_AUTH_POCKET_KEY = '' - -.. _Pocket API: http://getpocket.com/developer/ diff --git a/docs/backends/podio.rst b/docs/backends/podio.rst deleted file mode 100644 index 3f92e5e79..000000000 --- a/docs/backends/podio.rst +++ /dev/null @@ -1,13 +0,0 @@ -Podio -===== - -Podio offers OAuth2 as their auth mechanism. In order to enable it, follow: - -- Register a new application at `Podio API Keys`_ - -- Fill **Client Id** and **Client Secret** values:: - - SOCIAL_AUTH_PODIO_KEY = '' - SOCIAL_AUTH_PODIO_SECRET = '' - -.. _Podio API Keys: https://developers.podio.com/api-key diff --git a/docs/backends/qiita.rst b/docs/backends/qiita.rst deleted file mode 100644 index 512ca9d9f..000000000 --- a/docs/backends/qiita.rst +++ /dev/null @@ -1,23 +0,0 @@ -Qiita -===== - -Qiita - -- Register a new application at Qiita_, set the callback URL to - ``http://example.com/complete/qiita/`` replacing ``example.com`` with your - domain. - -- Fill ``Client ID`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_QIITA_KEY = '' - SOCIAL_AUTH_QIITA_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_QIITA_SCOPE = [...] - - See auth scopes at `Qiita Scopes docs`_. - - -.. _Qiita: https://qiita.com/settings/applications -.. _Qiita Scopes docs: https://qiita.com/api/v2/docs#スコープ diff --git a/docs/backends/qq.rst b/docs/backends/qq.rst deleted file mode 100644 index f0c1ef64a..000000000 --- a/docs/backends/qq.rst +++ /dev/null @@ -1,32 +0,0 @@ -QQ -== - -QQ implemented OAuth2 protocol for their authentication mechanism. To enable -``python-social-auth`` support follow this steps: - -1. Go to `QQ`_ and create an application. - -2. Fill App Id and Secret in your project settings:: - - SOCIAL_AUTH_QQ_KEY = '...' - SOCIAL_AUTH_QQ_SECRET = '...' - -3. Enable the backend:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.qq.QQOauth2', - ... - ) - - -The values for ``nickname``, ``figureurl_qq_1`` and ``gender`` will be stored -in the ``extra_data`` field. The ``nickname`` will be used as the account -username. ``figureurl_qq_1`` can be used as the profile image. - -Sometimes nickname will duplicate with another ``qq`` account, to avoid this -issue it's possible to use ``openid`` as ``username`` by define this setting:: - - SOCIAL_AUTH_QQ_USE_OPENID_AS_USERNAME = True - -.. _QQ: http://connect.qq.com/ diff --git a/docs/backends/rdio.rst b/docs/backends/rdio.rst deleted file mode 100644 index fb8437cb1..000000000 --- a/docs/backends/rdio.rst +++ /dev/null @@ -1,46 +0,0 @@ -Rdio -==== - -Rdio provides OAuth 1 and 2 support for their authentication process. - -OAuth 1.0a ----------- - -To setup Rdio OAuth 1.0a, add the following to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.rdio.RdioOAuth1', - ... - ) - - SOCIAL_AUTH_RDIO_OAUTH1_KEY = '' - SOCIAL_AUTH_RDIO_OAUTH1_SECRET = '' - - -OAuth 2.0 ---------- - -To setup Rdio OAuth 2.0, add the following to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.rdio.RdioOAuth2', - ... - ) - - SOCIAL_AUTH_RDIO_OAUTH2_KEY = os.environ['RDIO_OAUTH2_KEY'] - SOCIAL_AUTH_RDIO_OAUTH2_SECRET = os.environ['RDIO_OAUTH2_SECRET'] - SOCIAL_AUTH_RDIO_OAUTH2_SCOPE = [] - - -Extra Fields ------------- - -The following extra fields are automatically requested: - -- rdio_id -- rdio_icon_url -- rdio_profile_url -- rdio_username -- rdio_stream_region diff --git a/docs/backends/readability.rst b/docs/backends/readability.rst deleted file mode 100644 index bb98b0a89..000000000 --- a/docs/backends/readability.rst +++ /dev/null @@ -1,24 +0,0 @@ -Readability -=========== - -Readability works similarly to Twitter, in that you'll need a ``Consumer Key`` -and ``Consumer Secret``. These can be obtained in the ``Connections`` section -of your ``Account`` page. - -- Fill the **Consumer Key** and **Consumer Secret** values in your settings:: - - SOCIAL_AUTH_READABILITY_KEY = '' - SOCIAL_AUTH_READABILITY_SECRET = '' - -That's it! By default you'll get back:: - - username - first_name - last_name - -with EXTRA_DATA, you can get:: - - date_joined - kindle_email_address - avatar_url - email_into_address diff --git a/docs/backends/reddit.rst b/docs/backends/reddit.rst deleted file mode 100644 index 6f15242e8..000000000 --- a/docs/backends/reddit.rst +++ /dev/null @@ -1,33 +0,0 @@ -Reddit -====== - -Reddit implements `OAuth2 authentication workflow`_. To enable it, just follow: - -- Register an application at `Reddit Preferences Apps`_ - -- Fill the **Consumer Key** and **Consumer Secret** values in your settings:: - - SOCIAL_AUTH_REDDIT_KEY = '' - SOCIAL_AUTH_REDDIT_SECRET = '' - -- By default the token is not permanent, it will last an hour. To get - a refresh token just define:: - - SOCIAL_AUTH_REDDIT_AUTH_EXTRA_ARGUMENTS = {'duration': 'permanent'} - - This will store the ``refresh_token`` in ``UserSocialAuth.extra_data`` - attribute, to refresh the access token just do:: - - from social.apps.django_app.utils import load_strategy - - strategy = load_strategy(backend='reddit') - user = User.objects.get(pk=foo) - social = user.social_auth.filter(provider='reddit')[0] - social.refresh_token(strategy=strategy, - redirect_uri='http://localhost:8000/complete/reddit/') - - Reddit requires ``redirect_uri`` when refreshing the token and it must be the - same value used during the auth process. - -.. _Reddit Preferences Apps: https://ssl.reddit.com/prefs/apps/ -.. _OAuth2 authentication workflow: https://github.com/reddit/reddit/wiki/OAuth2 diff --git a/docs/backends/runkeeper.rst b/docs/backends/runkeeper.rst deleted file mode 100644 index 9c35a3859..000000000 --- a/docs/backends/runkeeper.rst +++ /dev/null @@ -1,13 +0,0 @@ -RunKeeper -========= - -RunKeeper uses OAuth v2 for authentication. - -- Register a new application at the `RunKeeper API`_, and - -- fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_RUNKEEPER_KEY = '' - SOCIAL_AUTH_RUNKEEPER_SECRET = '' - -.. _RunKeeper API: http://developer.runkeeper.com/healthgraph diff --git a/docs/backends/salesforce.rst b/docs/backends/salesforce.rst deleted file mode 100644 index 9b440a327..000000000 --- a/docs/backends/salesforce.rst +++ /dev/null @@ -1,44 +0,0 @@ -Salesforce -========== - -Salesforce uses OAuth v2 for Authentication, check the `official docs`_. - -- Create an app following the steps in the `Defining Connected Apps`_ docs. - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_SALESFORCE_OAUTH2_KEY = '' - SOCIAL_AUTH_SALESFORCE_OAUTH2_SECRET = '' - -- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.salesforce.SalesforceOAuth2', - ... - ) - -- Then you can start using ``{% url social:begin 'salesforce-oauth2' %}`` in - your templates - - -If using the sandbox mode: - -- Fill these settings instead:: - - SOCIAL_AUTH_SALESFORCE_OAUTH2_SANDBOX_KEY = '' - SOCIAL_AUTH_SALESFORCE_OAUTH2_SANDBOX_SECRET = '' - -- And this backend:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.salesforce.SalesforceOAuth2Sandbox', - ... - ) - -- Then you can start using ``{% url social:begin 'salesforce-oauth2-sandbox' %}`` - in your templates - -.. _official docs: https://www.salesforce.com/us/developer/docs/api_rest/Content/intro_understanding_web_server_oauth_flow.htm -.. _Defining Connected Apps: https://www.salesforce.com/us/developer/docs/api_rest/Content/intro_defining_remote_access_applications.htm diff --git a/docs/backends/saml.rst b/docs/backends/saml.rst deleted file mode 100644 index e4de6eb5e..000000000 --- a/docs/backends/saml.rst +++ /dev/null @@ -1,169 +0,0 @@ -SAML -==== - -The SAML backend allows users to authenticate with any provider that supports -the SAML 2.0 protocol (commonly used for corporate or academic single sign on). - -The SAML backend for python-social-auth allows your web app to act as a SAML -Service Provider. You can configure one or more SAML Identity Providers that -users can use for authentication. For example, if your users are students, you -could enable Harvard and MIT as identity providers, so that students of either -of those two universities can use their campus login to access your app. - -Required Dependency -------------------- - -You must install python-saml_ 2.1.3 or higher in order to use this backend. - -Required Configuration ----------------------- - -At a minimum, you must add the following to your project's settings: - -- ``SOCIAL_AUTH_SAML_SP_ENTITY_ID``: The SAML Entity ID for your app. This - should be a URL that includes a domain name you own. It doesn't matter what - the URL points to. Example: ``http://saml.yoursite.com`` - -- ``SOCIAL_AUTH_SAML_SP_PUBLIC_CERT``: The X.509 certificate string for the - key pair that your app will use. You can generate a new self-signed key pair - with:: - - openssl req -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.key - - The contents of ``saml.crt`` should then be used as the value of this setting - (you can omit the first and last lines, which aren't required). - -- ``SOCIAL_AUTH_SAML_SP_PRIVATE_KEY``: The private key to be used by your app. - If you used the example openssl command given above, set this to the contents - of ``saml.key`` (again, you can omit the first and last lines). - -- ``SOCIAL_AUTH_SAML_ORG_INFO``: A dictionary that contains information about - your app. You must specify values for English at a minimum. Each language's - entry should specify a ``name`` (not shown to the user), a ``displayname`` - (shown to the user), and a URL. See the following - example:: - - { - "en-US": { - "name": "example", - "displayname": "Example Inc.", - "url": "http://example.com", - } - } - -- ``SOCIAL_AUTH_SAML_TECHNICAL_CONTACT``: A dictionary with two values, - ``givenName`` and ``emailAddress``, describing the name and email of a - technical contact responsible for your app. Example:: - - {"givenName": "Tech Gal", "emailAddress": "technical@example.com"} - -- ``SOCIAL_AUTH_SAML_TECHNICAL_CONTACT``: A dictionary with two values, - ``givenName`` and ``emailAddress``, describing the name and email of a - support contact for your app. Example:: - - SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { - "givenName": "Support Guy", - "emailAddress": "support@example.com", - } - -- ``SOCIAL_AUTH_SAML_ENABLED_IDPS``: The most important setting. List the Entity - ID, SSO URL, and x.509 public key certificate for each provider that your app - wants to support. The SSO URL must support the ``HTTP-Redirect`` binding. - You can get these values from the provider's XML metadata. Here's an example, - for TestShib_ (the values come from TestShib's metadata_):: - - { - "testshib": { - "entity_id": "https://idp.testshib.org/idp/shibboleth", - "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", - "x509cert": "MIIEDjCCAvagAwIBAgIBADA ... 8Bbnl+ev0peYzxFyF5sQA==", - } - } - -Basic Usage ------------ - -- Set all of the required configuration variables described above. - -- Generate the SAML XML metadata for your app. The best way to do this is to - create a new view/page/URL in your app that will call the backend's - ``generate_metadata_xml()`` method. Here's an example of how to do this in - Django:: - - def saml_metadata_view(request): - complete_url = reverse('social:complete', args=("saml", )) - saml_backend = load_backend( - load_strategy(request), - "saml", - redirect_uri=complete_url, - ) - metadata, errors = saml_backend.generate_metadata_xml() - if not errors: - return HttpResponse(content=metadata, content_type='text/xml') - -- Download the metadata for your app that was generated by the above method, - and send it to each Identity Provider (IdP) that you wish to use. Each IdP - must install and configure your metadata on their system before it will work. - -- Now everything is set! To allow users to login with any given IdP, you need to - give them a link to the python-social-auth "begin"/"auth" URL and include an - ``idp`` query parameter that specifies the name of the IdP to use. This is - needed since the backend supports multiple IdPs. The names of the IdPs are the - keys used in the ``SOCIAL_AUTH_SAML_ENABLED_IDPS`` setting. - - Django example:: - - # In view: - context['testshib_url'] = u"{base}?{params}".format( - base=reverse('social:begin', kwargs={'backend': 'saml'}), - params=urllib.urlencode({'next': '/home', 'idp': 'testshib'}) - ) - # In template: - TestShib Login - # Result: - TestShib Login - -- Testing with the TestShib_ provider is recommended, as it is known to work - well. - - -Advanced Settings ------------------ - -- ``SOCIAL_AUTH_SAML_SP_EXTRA``: This can be set to a dict, and any key/value - pairs specified here will be passed to the underlying ``python-saml`` library - configuration's ``sp`` setting. Refer to the ``python-saml`` documentation for - details. - -- ``SOCIAL_AUTH_SAML_SECURITY_CONFIG``: This can be set to a dict, and any - key/value pairs specified here will be passed to the underlying - ``python-saml`` library configuration's ``security`` setting. Two useful keys - that you can set are ``metadataCacheDuration`` and ``metadataValidUntil``, - which control the expiry time of your XML metadata. By default, a cache - duration of 10 days will be used, which means that IdPs are allowed to cache - your metadata for up to 10 days, but no longer. ``metadataCacheDuration`` must - be specified as an ISO 8601 duration string (e.g. `P1D` for one day). - - -Advanced Usage --------------- - -You can subclass the ``SAMLAuth`` backend to provide custom functionality. In -particular, there are two methods that are designed for subclasses to override: - -- ``get_idp(self, idp_name)``: Given the name of an IdP, return an instance of - ``SAMLIdentityProvider`` with the details of the IdP. Override this method if - you wish to use some other method for configuring the available identity - providers, such as fetching them at runtime from another server, or using a - list of providers from a Shibboleth federation. - -- ``_check_entitlements(self, idp, attributes)``: This method gets called during - the login process and is where you can decide to accept or reject a user based - on the user's SAML attributes. For example, you can restrict access to your - application to only accept users who belong to a certain department. After - inspecting the passed attributes parameter, do nothing to allow the user to - login, or raise ``social.exceptions.AuthForbidden`` to reject the user. - -.. _python-saml: https://github.com/onelogin/python-saml -.. _TestShib: https://www.testshib.org/ -.. _metadata: https://www.testshib.org/metadata/testshib-providers.xml diff --git a/docs/backends/shopify.rst b/docs/backends/shopify.rst deleted file mode 100644 index fec9f8aa4..000000000 --- a/docs/backends/shopify.rst +++ /dev/null @@ -1,29 +0,0 @@ -Shopify -======= - -Shopify uses OAuth 2 for authentication. - -To use this backend, you must install the package ``shopify`` from the `Github -project`_. Currently supports v2+ - -- Register a new application at `Shopify Partners`_, and - -- Set the Auth Type to OAuth2 in the application settings - -- Set the Application URL to http://[your domain]/login/shopify/ - -- fill ``API Key`` and ``Shared Secret`` values in your django settings:: - - SOCIAL_AUTH_SHOPIFY_KEY = '' - SOCIAL_AUTH_SHOPIFY_SECRET = '' - -- fill the scope permissions that you require into the settings `Shopify API`_:: - - SOCIAL_AUTH_SHOPIFY_SCOPE = ['write_script_tags', - 'read_orders', - 'write_customers', - 'read_products'] - -.. _Shopify Partners: http://www.shopify.com/partners -.. _Shopify API: http://api.shopify.com/authentication.html#scopes -.. _Github project: https://github.com/Shopify/shopify_python_api diff --git a/docs/backends/sketchfab.rst b/docs/backends/sketchfab.rst deleted file mode 100644 index c69a388e2..000000000 --- a/docs/backends/sketchfab.rst +++ /dev/null @@ -1,17 +0,0 @@ -Sketchfab -========= - -Sketchfab uses OAuth 2 for authentication. - -To use: - -- Follow the steps at `Sketchfab Oauth`_, and ask for an - ``Authorization code`` grant type. - -- Fill the ``Client id/key`` and ``Client Secret`` values you received - in your django settings:: - - SOCIAL_AUTH_SKETCHFAB_KEY = '' - SOCIAL_AUTH_SKETCHFAB_SECRET = '' - -.. _Sketchfab Oauth: https://sketchfab.com/developers/oauth diff --git a/docs/backends/skyrock.rst b/docs/backends/skyrock.rst deleted file mode 100644 index 7cd693e7c..000000000 --- a/docs/backends/skyrock.rst +++ /dev/null @@ -1,21 +0,0 @@ -Skyrock -======= - -OAuth based Skyrock Connect. - -Skyrock offers per application keys named ``Consumer Key`` and ``Consumer -Secret``. To enable Skyrock these two keys are needed. Further documentation -at `Skyrock developer resources`_: - -- Register a new application at `Skyrock App Creation`_, - -- Your callback domain should match your application URL in your application - configuration. - -- Fill **Consumer Key** and **Consumer Secret** values:: - - SOCIAL_AUTH_SKYROCK_KEY = '' - SOCIAL_AUTH_SKYROCK_SECRET = '' - -.. _Skyrock developer resources: http://www.skyrock.com/developer/ -.. _Skyrock App Creation: https://wwwskyrock.com/developer/application diff --git a/docs/backends/slack.rst b/docs/backends/slack.rst deleted file mode 100644 index ce3ed29aa..000000000 --- a/docs/backends/slack.rst +++ /dev/null @@ -1,23 +0,0 @@ -Slack -===== - -Slack - -- Register a new application at Slack_, set the callback URL to - ``http://example.com/complete/slack/`` replacing ``example.com`` with your - domain. - -- Fill ``Client ID`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_SLACK_KEY = '' - SOCIAL_AUTH_SLACK_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_SLACK_SCOPE = [...] - - See auth scopes at `Slack OAuth docs`_. - - -.. _Slack: https://api.slack.com/applications -.. _Slack OAuth docs: https://api.slack.com/docs/oauth diff --git a/docs/backends/soundcloud.rst b/docs/backends/soundcloud.rst deleted file mode 100644 index a58b33679..000000000 --- a/docs/backends/soundcloud.rst +++ /dev/null @@ -1,25 +0,0 @@ -SoundCloud -========== - -SoundCloud uses OAuth2 for its auth mechanism. - -- Register a new application at `SoundCloud App Registration`_, set your - application name, website and redirect URI. - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_SOUNDCLOUD_KEY = '' - SOCIAL_AUTH_SOUNDCLOUD_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_SOUNDCLOUD_SCOPE = [...] - -Possible scope values are `*` or `non-expiring` according to their `/connect -documentation`_. - -Check the rest of their doc at `SoundCloud Developer Documentation`_. - -.. _SoundCloud App Registration: http://soundcloud.com/you/apps/new -.. _SoundCloud Developer Documentation: http://developers.soundcloud.com/docs -.. _/connect documentation: http://developers.soundcloud.com/docs/api/reference#connect diff --git a/docs/backends/spotify.rst b/docs/backends/spotify.rst deleted file mode 100644 index ca3ddd1f6..000000000 --- a/docs/backends/spotify.rst +++ /dev/null @@ -1,25 +0,0 @@ -Spotify -======= - -Spotify supports OAuth 2. - -- Register a new application at `Spotify Web API`_, and follow the - instructions below. - -OAuth2 ------- - -Add the Spotify OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.spotify.SpotifyOAuth2', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_SPOTIFY_KEY = '' - SOCIAL_AUTH_SPOTIFY_SECRET = '' - -.. _Spotify Web API: https://developer.spotify.com/spotify-web-api diff --git a/docs/backends/stackoverflow.rst b/docs/backends/stackoverflow.rst deleted file mode 100644 index 8c525b909..000000000 --- a/docs/backends/stackoverflow.rst +++ /dev/null @@ -1,19 +0,0 @@ -Stackoverflow -============= - -Stackoverflow uses OAuth 2.0 - -- "Register For An App Key" at the `Stack Exchange API`_ site. Set your OAuth - domain and application website settings. - -- Add the ``Client Id``, ``Client Secret`` and ``API Key`` values in settings:: - - SOCIAL_AUTH_STACKOVERFLOW_KEY = '' - SOCIAL_AUTH_STACKOVERFLOW_SECRET = '' - SOCIAL_AUTH_STACKOVERFLOW_API_KEY = '' - -- You can ask for extra permissions with:: - - SOCIAL_AUTH_STACKOVERFLOW_SCOPE = [...] - -.. _Stack Exchange API: https://api.stackexchange.com/ diff --git a/docs/backends/steam.rst b/docs/backends/steam.rst deleted file mode 100644 index de38c382c..000000000 --- a/docs/backends/steam.rst +++ /dev/null @@ -1,19 +0,0 @@ -Steam OpenId -============ - -Steam OpenId works quite straightforward, but to retrieve some user data (known -as ``player`` on Steam API) a Steam API Key is needed. - -Configurable settings: - -- Supply a Steam API Key from `Steam Dev`_:: - - SOCIAL_AUTH_STEAM_API_KEY = key - - -- To save ``player`` data provided by Steam into ``extra_data``:: - - SOCIAL_AUTH_STEAM_EXTRA_DATA = ['player'] - - -.. _Steam Dev: http://steamcommunity.com/dev/apikey diff --git a/docs/backends/stocktwits.rst b/docs/backends/stocktwits.rst deleted file mode 100644 index d0d668c8c..000000000 --- a/docs/backends/stocktwits.rst +++ /dev/null @@ -1,16 +0,0 @@ -StockTwits -========== - -StockTwits uses OAuth 2 for authentication. - -- Register a new application at https://stocktwits.com/developers/apps - -- Set the Website URL to http://[your domain]/ - -- fill ``Consumer Key`` and ``Consumer Secret`` values in your django settings:: - - SOCIAL_AUTH_STOCKTWITS_KEY = '' - SOCIAL_AUTH_STOCKTWITS_SECRET = '' - -.. _StockTwits authentication docs: http://stocktwits.com/developers/docs/authentication -.. _StockTwits API: http://stocktwits.com/developers/docs/api diff --git a/docs/backends/strava.rst b/docs/backends/strava.rst deleted file mode 100644 index a2086c980..000000000 --- a/docs/backends/strava.rst +++ /dev/null @@ -1,17 +0,0 @@ -Strava -========= - -Strava uses OAuth v2 for Authentication. - -- Register a new application at the `Strava API`_, and - -- fill ``Client ID`` and ``Client Secret`` from strava.com values in the settings:: - - SOCIAL_AUTH_STRAVA_KEY = '' - SOCIAL_AUTH_STRAVA_SECRET = '' - -- extra scopes can be defined by using:: - - SOCIAL_AUTH_STRAVA_SCOPE = ['view_private'] - -.. _Strava API: https://www.strava.com/settings/api diff --git a/docs/backends/stripe.rst b/docs/backends/stripe.rst deleted file mode 100644 index a74342c93..000000000 --- a/docs/backends/stripe.rst +++ /dev/null @@ -1,34 +0,0 @@ -Stripe -====== - -Stripe uses OAuth2 for its authorization service. To setup Stripe backend: - -- Register a new application at `Stripe App Creation`_, and - -- Grab the ``client_id`` value in ``Applications`` tab and fill the ``App Id`` - setting:: - - SOCIAL_AUTH_STRIPE_KEY = 'ca_...' - -- Grab the ``Test Secret Key`` in the ``API Keys`` tab and fille the ``App - Secret`` setting:: - - SOCIAL_AUTH_STRIPE_SECRET = '...' - -- Define ``SOCIAL_AUTH_STRIPE_SCOPE`` with the desired scope (options are - ``read_only`` and ``read_write``):: - - SOCIAL_AUTH_STRIPE_SCOPE = ['read_only'] - -- Add the needed backend to ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.stripe.StripeOAuth2', - ... - ) - -More info on Stripe OAuth2 at `Integrating OAuth`_. - -.. _Stripe App Creation: https://manage.stripe.com/#account/applications/settings -.. _Integrating OAuth: https://stripe.com/docs/connect/oauth diff --git a/docs/backends/suse.rst b/docs/backends/suse.rst deleted file mode 100644 index 239cc5fa9..000000000 --- a/docs/backends/suse.rst +++ /dev/null @@ -1,13 +0,0 @@ -SUSE -==== - -This section describes how to setup the different services provided by SUSE and openSUSE. - - -openSUSE OpenId ---------------- - -openSUSE OpenId works straightforward, not settings are needed. Domains or emails -whitelists can be applied too, check the whitelists_ settings for details. - -.. _whitelists: ../configuration/settings.html#whitelists diff --git a/docs/backends/taobao.rst b/docs/backends/taobao.rst deleted file mode 100644 index 9606ecf12..000000000 --- a/docs/backends/taobao.rst +++ /dev/null @@ -1,15 +0,0 @@ -Taobao OAuth -============ - -Taobao OAuth 2.0 workflow. - -- Register a new application at Open `Open Taobao`_. - -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - - SOCIAL_AUTH_TAOBAO_KEY = '' - SOCIAL_AUTH_TAOBAO_SECRET = '' - -By default ``token`` is stored in ``extra_data`` field. - -.. _Open Taobao: http://open.taobao.com diff --git a/docs/backends/thisismyjam.rst b/docs/backends/thisismyjam.rst deleted file mode 100644 index 02efa79c4..000000000 --- a/docs/backends/thisismyjam.rst +++ /dev/null @@ -1,17 +0,0 @@ -ThisIsMyJam -=========== - -ThisIsMyJam uses OAuth1 for its auth mechanism. - -- Register a new application at `ThisIsMyJam App Registration`_, set your - application name, website and redirect URI. - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_THISISMYJAM_KEY = '' - SOCIAL_AUTH_THISISMYJAM_SECRET = '' - -Check the rest of their doc at `ThisIsMyJam API Docs`_. - -.. _ThisIsMyJam App Registration: https://www.thisismyjam.com/developers -.. _ThisIsMyJam API Docs: https://www.thisismyjam.com/developers/docs diff --git a/docs/backends/trello.rst b/docs/backends/trello.rst deleted file mode 100644 index 4b55f9931..000000000 --- a/docs/backends/trello.rst +++ /dev/null @@ -1,26 +0,0 @@ -Trello -====== - -Trello provides OAuth1 support for their authentication process. - -In order to enable it, follow: - -- Generate an Application Key pair at `Trello Developers API Keys`_ - -- Fill **Consumer Key** and **Consumer Secret** settings:: - - SOCIAL_AUTH_TRELLO_KEY = '...' - SOCIAL_AUTH_TRELLO_SECRET = '...' - -There are also two optional settings: - -- your app name, otherwise the authorization page will say "Let An unknown application use your account?":: - - SOCIAL_AUTH_TRELLO_APP_NAME = 'My App' - -- the expiration period, social auth defaults to 'never', but you can change it:: - - SOCIAL_AUTH_TRELLO_EXPIRATION = '30days' - - -.. _Trello Developers API Keys: https://trello.com/1/appKey/generate diff --git a/docs/backends/tripit.rst b/docs/backends/tripit.rst deleted file mode 100644 index acc5cd64c..000000000 --- a/docs/backends/tripit.rst +++ /dev/null @@ -1,16 +0,0 @@ -TripIt -====== - -TripIt offers per application keys named ``API Key`` and ``API Secret``. -To enable TripIt these two keys are needed. Further documentation at -`TripIt Developer Center`_: - -- Register a new application at `TripIt App Registration`_, - -- fill **API Key** and **API Secret** values:: - - SOCIAL_AUTH_TRIPIT_KEY = '' - SOCIAL_AUTH_TRIPIT_SECRET = '' - -.. _TripIt Developer Center: https://www.tripit.com/developer -.. _TripIt App Registration: https://www.tripit.com/developer/create diff --git a/docs/backends/tumblr.rst b/docs/backends/tumblr.rst deleted file mode 100644 index 90c1aa046..000000000 --- a/docs/backends/tumblr.rst +++ /dev/null @@ -1,16 +0,0 @@ -Tumblr -====== - -Tumblr uses OAuth 1.0a for authentication. - -- Register a new application at http://www.tumblr.com/oauth/apps - -- Set the ``Default callback URL`` to http://[your domain]/ - -- fill ``OAuth Consumer Key`` and ``Secret Key`` values in your Django - settings:: - - SOCIAL_AUTH_TUMBLR_KEY = '' - SOCIAL_AUTH_TUMBLR_SECRET = '' - -.. _Tumblr API: http://www.tumblr.com/docs/en/api/v2 diff --git a/docs/backends/twilio.rst b/docs/backends/twilio.rst deleted file mode 100644 index 8f118dbb7..000000000 --- a/docs/backends/twilio.rst +++ /dev/null @@ -1,22 +0,0 @@ -Twilio -====== - -- Register a new application at `Twilio Connect Api`_ - -- Fill ``SOCIAL_AUTH_TWILIO_KEY`` and ``SOCIAL_AUTH_TWILIO_SECRET`` values in - the settings:: - - SOCIAL_AUTH_TWILIO_KEY = '' - SOCIAL_AUTH_TWILIO_SECRET = '' - -- Add desired authentication backends to Django's ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS`` - setting:: - - 'social.backends.twilio.TwilioAuth', - -- Usage example:: - - Enter using Twilio - - -.. _Twilio Connect API: https://www.twilio.com/user/account/connect/apps diff --git a/docs/backends/twitch.rst b/docs/backends/twitch.rst deleted file mode 100644 index bb99a9e2c..000000000 --- a/docs/backends/twitch.rst +++ /dev/null @@ -1,19 +0,0 @@ -Twitch -====== - -Twitch works similar to Facebook (OAuth). - -- Register a new application in the `connections tab`_ of your Twitch settings - page, set the callback URL to ``http://example.com/complete/twitch/`` - replacing ``example.com`` with your domain. - -- Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_TWITCH_KEY = '' - SOCIAL_AUTH_TWITCH_SECRET = '' - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_TWITCH_SCOPE = [...] - -.. _connections tab: http://www.twitch.tv/settings/connections diff --git a/docs/backends/twitter.rst b/docs/backends/twitter.rst deleted file mode 100644 index 64e885c1a..000000000 --- a/docs/backends/twitter.rst +++ /dev/null @@ -1,34 +0,0 @@ -Twitter -======= - -Twitter offers per application keys named ``Consumer Key`` and ``Consumer Secret``. -To enable Twitter these two keys are needed. Further documentation at -`Twitter development resources`_: - -- Register a new application at `Twitter App Creation`_, - -- Check the **Allow this application to be used to Sign in with Twitter** - checkbox. If you don't check this box, Twitter will force your user to login - every time. - -- Fill **Consumer Key** and **Consumer Secret** values:: - - SOCIAL_AUTH_TWITTER_KEY = '' - SOCIAL_AUTH_TWITTER_SECRET = '' - -- You need to specify an URL callback or the application will be marked as - Client type instead of the Browser. Almost any dummy value will work if - you plan some test. - -- You can request user's Email address (consult `Twitter verify - credentials`_), the parameter is sent automatically, but the - applicaton needs to be whitelisted in order to get a valid value. - -Twitter usually fails with a 401 error when trying to call the request-token -URL, this is usually caused by server datetime errors (check miscellaneous -section). Installing ``ntp`` and syncing the server date with some pool does -the trick. - -.. _Twitter development resources: http://dev.twitter.com/pages/auth -.. _Twitter App Creation: http://twitter.com/apps/new -.. _Twitter verify credentials: https://dev.twitter.com/rest/reference/get/account/verify_credentials diff --git a/docs/backends/uber.rst b/docs/backends/uber.rst deleted file mode 100644 index 7aca7b97e..000000000 --- a/docs/backends/uber.rst +++ /dev/null @@ -1,28 +0,0 @@ -Uber -========= - -Uber uses OAuth v2 for Authentication. - -- Register a new application at the `Uber API`_, and follow the instructions below - -OAuth2 -========= - -1. Add the Uber OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.uber.UberOAuth2', - ... - ) - -2. Fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_UBER_KEY = '' - SOCIAL_AUTH_UBER_SECRET = '' - -3. Scope should be defined by using:: - - SOCIAL_AUTH_UBER_SCOPE = ['profile', 'request'] - -.. _Uber API: https://developer.uber.com/dashboard diff --git a/docs/backends/untappd.rst b/docs/backends/untappd.rst deleted file mode 100644 index 0b41a1273..000000000 --- a/docs/backends/untappd.rst +++ /dev/null @@ -1,28 +0,0 @@ -Untappd -======= - -Untappd uses OAuth v2 for Authentication, check the `official docs`_. - -- Create an app by filling out the form here: `Add App`_ - -- Apps are approved on a one-by-one basis, so you'll need to wait a - few days to get your client ID and secret. - -- Fill ``Client ID`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_UNTAPPD_KEY = '' - SOCIAL_AUTH_UNTAPPD_SECRET = '' - -- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.untappd.UntappdOAuth2', - ... - ) - -- Then you can start using ``{% url social:begin 'untappd' %}`` in - your templates - -.. _official docs: https://untappd.com/api/docs -.. _Add App: https://untappd.com/api/register?register=new diff --git a/docs/backends/upwork.rst b/docs/backends/upwork.rst deleted file mode 100644 index 59b599096..000000000 --- a/docs/backends/upwork.rst +++ /dev/null @@ -1,28 +0,0 @@ -Upwork -====== - -Upwork supports only OAuth 1. - -- Register a new application at `Upwork Developers`_. - -OAuth1 ------- - -Add the Upwork OAuth backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.upwork.UpworkOAuth', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_UPWORK_KEY = '' - SOCIAL_AUTH_UPWORK_SECRET = '' - - -**Note:** For more information please go to `Upwork API Reference`_. - -.. _Upwork Developers: https://www.upwork.com/services/api/apply -.. _Upwork API Reference: https://developers.upwork.com/?lang=python diff --git a/docs/backends/username.rst b/docs/backends/username.rst deleted file mode 100644 index dcd45ef2c..000000000 --- a/docs/backends/username.rst +++ /dev/null @@ -1,52 +0,0 @@ -Username Auth -============= - -python-social-auth_ comes with an UsernameAuth_ backend which comes handy when -your site uses requires the plain old username and password authentication -mechanism. - -Actually that's a lie since the backend doesn't handle password at all, that's -up to the developer to validate the password in and the proper place to do it -is the pipeline, right after the user instance was retrieved or created. - -The reason to leave password handling to the developer is because too many -things are really tied to the project, like the field where the password is -stored, salt handling, password hashing algorithm and validation. So just add -the pipeline functions that will do that following the needs of your project. - - -Backend settings ----------------- - -``SOCIAL_AUTH_USERNAME_FORM_URL = '/login-form/'`` - Used to redirect the user to the login/signup form, it must have at least - one field named ``username``. Form submit should go to ``/complete/username``, - or if it goes to your view, then your view should complete the process - calling ``social.actions.do_complete``. - -``SOCIAL_AUTH_USERNAME_FORM_HTML = 'login_form.html'`` - The template will be used to render the login/signup form to the user, it - must have at least one field named ``username``. Form submit should go to - ``/complete/username``, or if it goes to your view, then your view should - complete the process calling ``social.actions.do_complete``. - - -Password handling ------------------ - -Here's an example of password handling to add to the pipeline:: - - def user_password(strategy, user, is_new=False, *args, **kwargs): - if strategy.backend.name != 'username': - return - - password = strategy.request_data()['password'] - if is_new: - user.set_password(password) - user.save() - elif not user.validate_password(password): - # return {'user': None, 'social': None} - raise AuthException(strategy.backend) - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _UsernameAuth: https://github.com/omab/python-social-auth/blob/master/social/backends/username.py#L5 diff --git a/docs/backends/vend.rst b/docs/backends/vend.rst deleted file mode 100644 index 880698e59..000000000 --- a/docs/backends/vend.rst +++ /dev/null @@ -1,24 +0,0 @@ -Vend -==== - -Vend supports OAuth 2. - -- Register a new application at `Vend Developers Portal`_ - -- Add the Vend OAuth2 backend to your settings page:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.vend.VendOAuth2', - ... - ) - -- Fill ``App Key`` and ``App Secret`` values in the settings:: - - SOCIAL_AUTH_VEND_OAUTH2_KEY = '' - SOCIAL_AUTH_VEND_OAUTH2_SECRET = '' - -More details on their docs_. - -.. _Vend Developers Portal: https://developers.vendhq.com/developer/applications -.. _docs: https://developers.vendhq.com/documentation diff --git a/docs/backends/vimeo.rst b/docs/backends/vimeo.rst deleted file mode 100644 index b2e4bd089..000000000 --- a/docs/backends/vimeo.rst +++ /dev/null @@ -1,28 +0,0 @@ -Vimeo -===== - -Vimeo uses OAuth1 to grant access to their API. In order to get the backend -running follow: - -- Register an application at `Vimeo Developer Portal`_ filling the required - settings. Ensure to fill ``App Callback URL`` field with - ``http:///complete/vimeo/`` - -- Fill in the **Client Id** and **Client Secret** values in your settings:: - - SOCIAL_AUTH_VIMEO_KEY = '' - SOCIAL_AUTH_VIMEO_SECRET = '' - -- Specify scopes with:: - - SOCIAL_AUTH_VIMEO_SCOPE = [...] - -- Add the backend to ``AUTHENTICATION_BACKENDS``:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.vimeo.VimeoOAuth1', - ... - ) - -.. _Vimeo Developer Portal: https://developer.vimeo.com/apps/new diff --git a/docs/backends/vk.rst b/docs/backends/vk.rst deleted file mode 100644 index a9195c76b..000000000 --- a/docs/backends/vk.rst +++ /dev/null @@ -1,131 +0,0 @@ -VK.com (former Vkontakte) -========================= - -VK.com (former Vkontakte) auth service support. - -OAuth2 ------- - -VK.com uses OAuth2 for Authentication. - -- Register a new application at the `VK.com API`_, - -- fill ``Application Id`` and ``Application Secret`` values in the settings:: - - SOCIAL_AUTH_VK_OAUTH2_KEY = '' - SOCIAL_AUTH_VK_OAUTH2_SECRET = '' - -- Add ``'social.backends.vk.VKOAuth2'`` into your ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``. - -- Then you can start using ``/login/vk-oauth2`` in your link href. - -- Also it's possible to define extra permissions with:: - - SOCIAL_AUTH_VK_OAUTH2_SCOPE = [...] - - See the `VK.com list of permissions`_. - - -OAuth2 Application ------------------- - -To support OAuth2 authentication for VK.com applications: - -- Create your IFrame application at VK.com. - -- In application settings specify your IFrame URL ``mysite.com/vk`` (current - default). - -- Fill ``Application ID`` and ``Application Secret`` settings:: - - SOCIAL_AUTH_VK_APP_KEY = '' - SOCIAL_AUTH_VK_APP_SECRET = '' - -- Fill ``user_mode``:: - - SOCIAL_AUTH_VK_APP_USER_MODE = 2 - - Possible values: - - ``0``: there will be no check whether a user connected to your - application or not - - ``1``: ``python-social-auth`` will check ``is_app_user`` parameter - VK.com sends when user opens application page one time - - ``2``: (safest) ``python-social-auth`` will check status of user - interactively (useful when you have interactive authentication via AJAX) - -- Add a snippet similar to this into your login template:: - - - - Click to authenticate - -To test, launch the server using ``sudo ./manage.py mysite.com:80`` for -browser to be able to load it when VK.com calls IFrame URL. Open your -VK.com application page via http://vk.com/app. Now you are able to -connect to application and login automatically after connection when visiting -application page. - -For more details see `authentication for VK.com applications`_ - - -OpenAPI -------- - -You can also use VK.com's own OpenAPI to log in, but you need to provide -HTML template with JavaScript code to authenticate, check below for an example. - -- Get an OpenAPI App Id and add it to the settings:: - - SOCIAL_AUTH_VK_OPENAPI_ID = '' - - This app id will be passed to the template as ``VK_APP_ID``. - -Snippet example:: - - - - Click to authorize - - -.. _VK.com OAuth: http://vk.com/dev/authentication -.. _VK.com list of permissions: http://vk.com/dev/permissions -.. _VK.com API: http://vk.com/dev/methods -.. _authentication for VK.com applications: http://www.ikrvss.ru/2011/11/08/django-social-auh-and-vkontakte-application/ diff --git a/docs/backends/weibo.rst b/docs/backends/weibo.rst deleted file mode 100644 index 4d933868c..000000000 --- a/docs/backends/weibo.rst +++ /dev/null @@ -1,23 +0,0 @@ -Weibo OAuth -=========== - -Weibo OAuth 2.0 workflow. - -- Register a new application at Weibo_. - -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - - SOCIAL_AUTH_WEIBO_KEY = '' - SOCIAL_AUTH_WEIBO_SECRET = '' - -By default ``account id``, ``profile_image_url`` and ``gender`` are stored in -extra_data field. - -The user name is used by default to build the user instance ``username``, -sometimes this contains non-ASCII characters which might not be desirable for -the website. To avoid this issue it's possible to use the Weibo ``domain`` -which will be inside the ASCII range by defining this setting:: - - SOCIAL_AUTH_WEIBO_DOMAIN_AS_USERNAME = True - -.. _Weibo: http://open.weibo.com diff --git a/docs/backends/withings.rst b/docs/backends/withings.rst deleted file mode 100644 index a9f5a6ea7..000000000 --- a/docs/backends/withings.rst +++ /dev/null @@ -1,13 +0,0 @@ -Withings -======== - -Withings uses OAuth v1 for Authentication. - -- Register a new application at the `Withings API`_, and - -- fill ``Client ID`` and ``Client Secret`` from withings.com values in the settings:: - - SOCIAL_AUTH_WITHINGS_KEY = '' - SOCIAL_AUTH_WITHINGS_SECRET = '' - -.. _Withings API: https://oauth.withings.com/partner/add diff --git a/docs/backends/wunderlist.rst b/docs/backends/wunderlist.rst deleted file mode 100644 index 218686d44..000000000 --- a/docs/backends/wunderlist.rst +++ /dev/null @@ -1,13 +0,0 @@ -Wunderlist -========== - -Wunderlist uses OAuth v2 for Authentication. - -- Register a new application at `Wunderlist Developer Portal`_, and - -- fill ``Client Id`` and ``Client Secret`` values in the settings:: - - SOCIAL_AUTH_WUNDERLIST_KEY = '' - SOCIAL_AUTH_WUNDERLIST_SECRET = '' - -.. _Wunderlist Developer Portal: https://developer.wunderlist.com/applications diff --git a/docs/backends/xing.rst b/docs/backends/xing.rst deleted file mode 100644 index 3454b116b..000000000 --- a/docs/backends/xing.rst +++ /dev/null @@ -1,14 +0,0 @@ -XING -==== - -XING uses OAuth1 for their auth mechanism, in order to enable the backend -follow: - -- Register a new application at `XING Apps Dashboard`_, - -- Fill **Consumer Key** and **Consumer Secret** values:: - - SOCIAL_AUTH_XING_KEY = '' - SOCIAL_AUTH_XING_SECRET = '' - -.. _XING Apps Dashboard: https://dev.xing.com/applications diff --git a/docs/backends/yahoo.rst b/docs/backends/yahoo.rst deleted file mode 100644 index 01a0c7e37..000000000 --- a/docs/backends/yahoo.rst +++ /dev/null @@ -1,33 +0,0 @@ -Yahoo -===== - -Yahoo supports OpenId and OAuth2 for their auth flow. - - -Yahoo OpenId ------------- - -OpenId doesn't require any particular configuration beside enabling the backend -in the ``AUTHENTICATION_BACKENDS`` setting:: - - AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.yahoo.YahooOpenId', - ... - ) - - -Yahoo OAuth2 ------------- -OAuth 2.0 workflow, useful if you are planning to use Yahoo's API. - -- Register a new application at `Yahoo Developer Center`_, set your app domain - and configure scopes (they can't be overriden by application). - -- Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - - SOCIAL_AUTH_YAHOO_OAUTH2_KEY = '' - SOCIAL_AUTH_YAHOO_OAUTH2_SECRET = '' - - -.. _Yahoo Developer Center: https://developer.yahoo.com/ diff --git a/docs/backends/yammer.rst b/docs/backends/yammer.rst deleted file mode 100644 index 86f4c74c3..000000000 --- a/docs/backends/yammer.rst +++ /dev/null @@ -1,30 +0,0 @@ -Yammer -====== - -Yammer users OAuth2 for their auth mechanism, this application supports Yammer -OAuth2 in production and staging modes. - -Production Mode ---------------- - -In order to enable the backend, follow: - - -- Register an application at `Client Applications`_, - set the ``Redirect URI`` to ``http:///complete/yammer/`` - -- Fill **Client Key** and **Client Secret** settings:: - - SOCIAL_AUTH_YAMMER_KEY = '...' - SOCIAL_AUTH_YAMMER_SECRET = '...' - - -Staging Mode ------------- - -Staging mode is configured the same as ``Production Mode``, but settings are -prefixed with:: - - SOCIAL_AUTH_YAMMER_STAGING_* - -.. _Client Applications: https://www.yammer.com/client_applications diff --git a/docs/backends/zotero.rst b/docs/backends/zotero.rst deleted file mode 100644 index 19da98221..000000000 --- a/docs/backends/zotero.rst +++ /dev/null @@ -1,25 +0,0 @@ -Zotero -====== - -Zotero implements OAuth1 as their authentication mechanism for their Web API v3. - - -1. Go to the `Zotero app registration page`_ to register your application. - -2. Fill the **Client ID** and **Client Secret** in your project settings:: - - SOCIAL_AUTH_ZOTERO_KEY = '...' - SOCIAL_AUTH_ZOTERO_SECRET = '...' - -3. Enable the backend:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - ... - 'social.backends.zotero.ZoteroOAuth', - ... - ) - -Further documentation at `Zotero Web API v3 page`_. - -.. _Zotero app registration page: https://www.zotero.org/oauth/apps -.. _Zotero Web API v3 page: https://www.zotero.org/support/dev/web_api/v3/start diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index e8692b23f..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', 'sphinx.ext.viewcode'] -templates_path = ['_templates'] -source_suffix = '.rst' -master_doc = 'index' -project = u'Python Social Auth' -copyright = u'2012, Matías Aguirre' -exclude_patterns = ['_build'] -pygments_style = 'sphinx' -html_theme = 'nature' -html_static_path = [] -htmlhelp_basename = 'PythonSocialAuthdoc' -latex_documents = [ - ('index', 'PythonSocialAuth.tex', u'Python Social Auth Documentation', - u'Matías Aguirre', 'manual'), -] -man_pages = [ - ('index', 'pythonsocialauth', u'Python Social Auth Documentation', - [u'Matías Aguirre'], 1) -] -intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/docs/configuration/cherrypy.rst b/docs/configuration/cherrypy.rst deleted file mode 100644 index bd72659e7..000000000 --- a/docs/configuration/cherrypy.rst +++ /dev/null @@ -1,81 +0,0 @@ -CherryPy Framework -================== - -CherryPy framework is supported, it works but I'm sure there's room for -improvements. The implementation uses SQLAlchemy as ORM and expects some values -accessible on ``cherrypy.request`` for it to work. - -At the moment the configuration is expected on ``cherrypy.config`` but ideally -it should be an application configuration instead. - -Expected values are: - -``cherrypy.request.user`` - Current logged in user, load it in your application on a ``before_handler`` - handler. - -``cherrypy.request.db`` - Current database session, again, load it in your application on - a ``before_handler``. - - -Dependencies ------------- - -The `CherryPy built-in application` depends on sqlalchemy_, there's no support for -others ORMs yet but pull-requests are welcome. - - -Enabling the application ------------------------- - -The application is defined on ``social.apps.cherrypy_app.views.CherryPyPSAViews``, -register it in the preferred way for your project. - -Check the rest of the docs for the other settings like enabling authentication -backends and backends keys. - - -Models Setup ------------- - -The models are located in ``social.apps.cherrypy_app.models``. A reference to -your ``User`` model is required to be defined in the project settings, it -should be an import path, for example:: - - cherrypy.config.update({ - 'SOCIAL_AUTH_USER_MODEL': 'models.User' - }) - - -Login mechanism ---------------- - -By default the application sets the session value ``user_id``, this is a simple -solution and it should be improved, if you want to provider your own login -mechanism you can do it by defining the ``SOCIAL_AUTH_LOGIN_METHOD`` setting, -it should be an import path to a callable, like this:: - - SOCIAL_AUTH_USER_MODEL = 'app.login_user' - -And an example of this function:: - - def login_user(strategy, user): - strategy.session_set('user_id', user.id) - -Then, ensure to load the user in your application at ``cherrypy.request.user``, -for example:: - - def load_user(): - user_id = cherrypy.session.get('user_id') - if user_id: - cherrypy.request.user = cherrypy.request.db.query(User).get(user_id) - else: - cherrypy.request.user = None - - - cherrypy.tools.authenticate = cherrypy.Tool('before_handler', load_user) - - -.. _CherryPy built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/cherrypy_app -.. _sqlalchemy: http://www.sqlalchemy.org/ diff --git a/docs/configuration/django.rst b/docs/configuration/django.rst deleted file mode 100644 index 7aeef15be..000000000 --- a/docs/configuration/django.rst +++ /dev/null @@ -1,213 +0,0 @@ -Django Framework -================ - -Django framework has a little more support since this application was derived -from `django-social-auth`_. Here are some details on configuring this -application on Django. - - -Register the application ------------------------- - -The `Django built-in app`_ comes with two ORMs, one for default Django ORM and -another for MongoEngine_ ORM. - -Add the application to ``INSTALLED_APPS`` setting, for default ORM:: - - INSTALLED_APPS = ( - ... - 'social.apps.django_app.default', - ... - ) - -And for MongoEngine_ ORM:: - - INSTALLED_APPS = ( - ... - 'social.apps.django_app.me', - ... - ) - -Also ensure to define the MongoEngine_ storage setting:: - - SOCIAL_AUTH_STORAGE = 'social.apps.django_app.me.models.DjangoStorage' - - -Database --------- - -(For Django 1.7 and higher) sync database to create needed models:: - - ./manage.py migrate - -If you're still using South, you'll need override SOUTH_MIGRATION_MODULES_:: - - SOUTH_MIGRATION_MODULES = { - 'default': 'social.apps.django_app.default.south_migrations' - } - -Note that Django's app labels take the last part of the import, so -in this case ``social.apps.django_app.default`` becomes ``default`` here. - -Sync database to create needed models:: - - ./manage.py syncdb - - -Authentication backends ------------------------ - -Add desired authentication backends to Django's AUTHENTICATION_BACKENDS_ -setting:: - - AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - ... - 'django.contrib.auth.backends.ModelBackend', - ) - -Take into account that backends **must** be defined in AUTHENTICATION_BACKENDS_ -or Django won't pick them when trying to authenticate the user. - -Don't miss ``django.contrib.auth.backends.ModelBackend`` if using ``django.contrib.auth`` -application or users won't be able to login by username / password method. - - -URLs entries ------------- - -Add URLs entries:: - - urlpatterns = patterns('', - ... - url('', include('social.apps.django_app.urls', namespace='social')) - ... - ) - -In case you need a custom namespace, this setting is also needed:: - - SOCIAL_AUTH_URL_NAMESPACE = 'social' - - -Template Context Processors ---------------------------- - -There's a context processor that will add backends and associations data to -template context:: - - TEMPLATE_CONTEXT_PROCESSORS = ( - ... - 'social.apps.django_app.context_processors.backends', - 'social.apps.django_app.context_processors.login_redirect', - ... - ) - -``backends`` context processor will load a ``backends`` key in the context with -three entries on it: - -``associated`` - It's a list of ``UserSocialAuth`` instances related with the currently - logged in user. Will be empty if there's no current user. - -``not_associated`` - A list of available backend names not associated with the current user yet. - If there's no user logged in, it will be a list of all available backends. - -``backends`` - A list of all available backend names. - - -ORMs ----- - -As detailed above the built-in Django application supports default ORM and -MongoEngine_ ORM. - -When using MongoEngine_ make sure you've followed the instructions for -`MongoEngine Django integration`_, as you're now utilizing that user model. The -`MongoEngine_` backend was developed and tested with version 0.6.10 of -`MongoEngine_`. - -Alternate storage models implementations currently follow a tight pattern of -models that behave near or identical to Django ORM models. It is currently -not decoupled from this pattern by any abstraction layer. If you would like -to implement your own alternate, please see the -``social.apps.django_app.default.models`` and -``social.apps.django_app.me.models`` modules for guidance. - - -Exceptions Middleware ---------------------- - -A base middleware is provided that handles ``SocialAuthBaseException`` by -providing a message to the user via the Django messages framework, and then -responding with a redirect to a URL defined in one of the middleware methods. - -The middleware is at ``social.apps.django_app.middleware.SocialAuthExceptionMiddleware``. -Any method can be overridden, but for simplicity these two are recommended:: - - get_message(request, exception) - get_redirect_uri(request, exception) - -By default, the message is the exception message and the URL for the redirect -is the location specified by the ``LOGIN_ERROR_URL`` setting. - -If a valid backend was detected by ``strategy()`` decorator, it will be -available at ``request.strategy.backend`` and ``process_exception()`` will -use it to build a backend-dependent redirect URL but fallback to default if not -defined. - -Exception processing is disabled if any of this settings is defined with a -``True`` value:: - - _SOCIAL_AUTH_RAISE_EXCEPTIONS = True - SOCIAL_AUTH_RAISE_EXCEPTIONS = True - RAISE_EXCEPTIONS = True - DEBUG = True - -The redirect destination will get two ``GET`` parameters: - -``message = ''`` - Message from the exception raised, in some cases it's the message returned - by the provider during the auth process. - -``backend = ''`` - Backend name that was used, if it was a valid backend. - - -Django Admin ------------- - -The default application (not the MongoEngine_ one) contains an ``admin.py`` -module that will be auto-discovered by the usual mechanism. - -But, by the nature of the application which depends on the existence of a user -model, it's easy to fall in a recursive import ordering making the application -fail to load. This happens because the admin module will build a set of fields -to populate the ``search_fields`` property to search for related users in the -administration UI, but this requires the user model to be retrieved which might -not be defined at that time. - -To avoid this issue define the following setting to circumvent the import -error:: - - SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['field1', 'field2'] - -For example:: - - SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['username', 'first_name', 'email'] - -The fields listed **must** be user models fields. - -.. _MongoEngine: http://mongoengine.org -.. _MongoEngine Django integration: http://mongoengine-odm.readthedocs.org/en/latest/django.html -.. _django-social-auth: https://github.com/omab/django-social-auth -.. _Django built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/django_app -.. _AUTHENTICATION_BACKENDS: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#authentication-backends -.. _django@dc43fbc: https://github.com/django/django/commit/dc43fbc2f21c12e34e309d0e8a121020391aa03a -.. _SOUTH_MIGRATION_MODULES: http://south.readthedocs.org/en/latest/settings.html#south-migration-modules diff --git a/docs/configuration/flask.rst b/docs/configuration/flask.rst deleted file mode 100644 index fc7ada33d..000000000 --- a/docs/configuration/flask.rst +++ /dev/null @@ -1,160 +0,0 @@ -Flask Framework -=============== - -Flask reusable applications are tricky (or I'm not capable enough). Here are -details on how to enable this application on Flask. - - -Dependencies ------------- - -The `Flask built-in app` depends on sqlalchemy_, there's initial support for -MongoEngine_ ORM too (check below for more details). - - -Enabling the application ------------------------- - -The applications define a `Flask Blueprint`_, which needs to be registered once -the Flask app is configured by:: - - from social.apps.flask_app.routes import social_auth - - app.register_blueprint(social_auth) - -For MongoEngine_ you need this setting:: - - SOCIAL_AUTH_STORAGE = 'social.apps.flask_app.me.models.FlaskStorage' - - -Models Setup ------------- - -At the moment the models for python-social-auth_ are defined inside a function -because they need the reference to the current db instance and the User model -used on your project (check *User model reference* below). Once the Flask app -and the database are defined, call ``init_social`` to register the models:: - - from social.apps.flask_app.default.models import init_social - - init_social(app, db) - -For MongoEngine_:: - - from social.apps.flask_app.me.models import init_social - - init_social(app, db) - -So far I wasn't able to find another way to define the models on another way -rather than making it as a side-effect of calling this function since the -database is not available and ``current_app`` cannot be used on init time, just -run time. - - -User model reference --------------------- - -The application keeps a reference to the User model used by your project, -define it by using this setting:: - - SOCIAL_AUTH_USER_MODEL = 'foobar.models.User' - -The value must be the import path to the User model. - - -Global user ------------ - -The application expects the current logged in user accesible at ``g.user``, -define a handler like this to ensure that:: - - @app.before_request - def global_user(): - g.user = get_current_logged_in_user - - -Flask-Login ------------ - -The application works quite well with Flask-Login_, ensure to have some similar -handlers to these:: - - @login_manager.user_loader - def load_user(userid): - try: - return User.query.get(int(userid)) - except (TypeError, ValueError): - pass - - - @app.before_request - def global_user(): - g.user = login.current_user - - - # Make current user available on templates - @app.context_processor - def inject_user(): - try: - return {'user': g.user} - except AttributeError: - return {'user': None} - - -Remembering sessions --------------------- - -The users session can be remembered when specified on login. The common -implementation for this feature is to pass a parameter from the login form -(``remember_me``, ``keep``, etc), to flag the action. Flask-Login_ will mark -the session as persistent if told so. - -python-social-auth_ will check for a given name (``keep``) by default, but -since providers won't pass parameters back to the application, the value must -be persisted in the session before the authentication process happens. - -So, the following setting is required for this to work:: - - SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['keep'] - -It's possible to override the default name with this setting:: - - SOCIAL_AUTH_REMEMBER_SESSION_NAME = 'remember_me' - -Don't use the value ``remember`` since that will clash with Flask-Login_ which -pops the value from the session. - -Then just pass the parameter ``keep=1`` as a GET or POST parameter. - - -Exceptions handling -------------------- - -The Django application has a middleware (that fits in the framework -architecture) to facilitate the different exceptions_ handling raised by -python-social-auth_. The same can be accomplished (even on a simpler way) in -Flask by defining an errorhandler_. For example the next code will redirect any -social-auth exception to a ``/socialerror`` URL:: - - from social.exceptions import SocialAuthBaseException - - - @app.errorhandler(500) - def error_handler(error): - if isinstance(error, SocialAuthBaseException): - return redirect('/socialerror') - - -Be sure to set your debug and test flags to ``False`` when testing this on your -development environment, otherwise the exception will be raised and error -handlers won't be called. - - -.. _Flask Blueprint: http://flask.pocoo.org/docs/blueprints/ -.. _Flask-Login: https://github.com/maxcountryman/flask-login -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _Flask built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/flask_app -.. _sqlalchemy: http://www.sqlalchemy.org/ -.. _exceptions: https://github.com/omab/python-social-auth/blob/master/social/exceptions.py -.. _errorhandler: http://flask.pocoo.org/docs/api/#flask.Flask.errorhandler -.. _MongoEngine: http://mongoengine.org diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst deleted file mode 100644 index 92dc283f3..000000000 --- a/docs/configuration/index.rst +++ /dev/null @@ -1,24 +0,0 @@ -Configuration -============= - -All the apps share the settings names, some settings for Django framework are -special (like ``AUTHENTICATION_BACKENDS``). - -Below there's a main settings document detailing each configuration and its -purpose, plus sections detailed for each framework and their particularities. - -Support for more frameworks will be added in the future, pull-requests are very -welcome. - -Contents: - -.. toctree:: - :maxdepth: 2 - - settings - django - flask - pyramid - cherrypy - webpy - porting_from_dsa diff --git a/docs/configuration/porting_from_dsa.rst b/docs/configuration/porting_from_dsa.rst deleted file mode 100644 index aea7e72dc..000000000 --- a/docs/configuration/porting_from_dsa.rst +++ /dev/null @@ -1,145 +0,0 @@ -Porting from django-social-auth -=============================== - - -Being a derivative work from django-social-auth_, porting from it to -python-social-auth_ should be an easy task. Porting to others libraries usually -is a pain, I'm trying to make this as easy as possible. - - -Installed apps --------------- - -On django-social-auth_ there was a single application to add into -``INSTALLED_APPS`` plus a setting to define which ORM to be used (default or -MongoEngine). Now the apps are split and there's not need for that extra -setting. - -When using the default ORM:: - - INSTALLED_APPS = ( - ... - 'social.apps.django_app.default', - ... - ) - -And when using MongoEngine:: - - INSTALLED_APPS = ( - ... - 'social.apps.django_app.me', - ... - ) - -The models table names were defined to be compatible with those used on -django-social-auth_, so data is not needed to be migrated. - - -URLs ----- - -The URLs are namespaced, you can chose your namespace, the `example app`_ uses -the ``social`` namespace. Replace the old include with:: - - urlpatterns = patterns('', - ... - url('', include('social.apps.django_app.urls', namespace='social')) - ... - ) - -On templates use a namespaced URL:: - - {% url 'social:begin' "google-oauth2" %} - -Account disconnection URL would be:: - - {% url 'social:disconnect_individual' provider, id %} - - -Porting settings ----------------- - -All python-social-auth_ settings are prefixed with ``SOCIAL_AUTH_``, except for -some exception on Django framework, ``AUTHENTICATION_BACKENDS`` remains the -same for obvious reasons. - -All backends settings have the backend name into it, all uppercase and with -dashes replaced with underscores, take for instance Google OAuth2 backend is -named ``google-oauth2``, any setting name related to that backend should start -with ``SOCIAL_AUTH_GOOGLE_OAUTH2_``. - -Keys and secrets are some mandatory settings needed for OAuth providers, to -keep consistency the names follow the same naming convention ``*_KEY`` for the -application key, and ``*_SECRET`` for the secret. OAuth1 backends use to have -``CONSUMER`` in the setting name, not anymore. Following with the Google OAuth2 -example:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '...' - SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '...' - -Remember that the name of the backend is needed in the settings, and names -differ a little from backend to backend, like `Facebook OAuth2 backend`_ name -is ``facebook``. So the settings should be:: - - SOCIAL_AUTH_FACEBOOK_KEY = '...' - SOCIAL_AUTH_FACEBOOK_SECRET = '...' - - -Authentication backends ------------------------ - -Import path for authentication backends changed a little, there's no more -``contrib`` module, there's no need for it. Some backends changed the names to -have some consistency, check the backends, it should be easy to track the names -changes. Examples of the new import paths:: - - AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.facebook.FacebookOAuth2', - ) - - -Session -------- - -Django stores the last authentication backend used in the user session as an -import path, this can cause import troubles when porting since the old import -paths aren't valid anymore. Some solutions to this problem are: - -1. Clean the session and force the users to login again in your site - -2. Run a migration script that will update the authentication backend session - value for each session in your database. This implies figuring out the new - import path for each backend you have configured, which is the value used in - ``AUTHENTICATION_BACKENDS`` setting. - - `@tomgruner`_ created a Gist here_ that updates the value just for Facebook - backend. A ``template`` for this script would look like this:: - - from django.contrib.sessions.models import Session - - BACKENDS = { - 'social_auth.backends.facebook.FacebookBackend': 'social.backends.facebook.FacebookOAuth2' - } - - for sess in Session.objects.iterator(): - session_dict = sess.get_decoded() - - if '_auth_user_backend' in session_dict.keys(): - # Change old backend import path from new backend import path - if session_dict['_auth_user_backend'].startswith('social_auth'): - session_dict['_auth_user_backend'] = BACKENDS[session_dict['_auth_user_backend']] - new_sess = Session.objects.save(sess.session_key, session_dict, sess.expire_date) - print 'New session saved {}'.format(new_sess.pk) - - -.. _django-social-auth: https://github.com/omab/django-social-auth -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _example app: https://github.com/omab/python-social-auth/blob/master/examples/django_example/example/urls.py#L17 -.. _Facebook OAuth2 backend: https://github.com/omab/python-social-auth/blob/master/social/backends/facebook.py#L29 -.. _@tomgruner: https://github.com/tomgruner -.. _here: https://gist.github.com/tomgruner/5ce8bb1f4c55d17b5b25 diff --git a/docs/configuration/pyramid.rst b/docs/configuration/pyramid.rst deleted file mode 100644 index 7767ed614..000000000 --- a/docs/configuration/pyramid.rst +++ /dev/null @@ -1,137 +0,0 @@ -Pyramid Framework -================= - -Pyramid_ reusable applications are tricky (or I'm not capable enough). Here are -details on how to enable this application on Pyramid. - - -Dependencies ------------- - -The `Pyramid built-in app`_ depends on sqlalchemy_, there's no support for others -ORMs yet but pull-requests are welcome. - - -Enabling the application ------------------------- - -The application can be scanned by ``Configurator.scan()``, also it defines an -``includeme()`` in the ``__init__.py`` file which will add the needed routes to -your application configuration. To scan it just add:: - - config.include('social.apps.pyramid_app') - config.scan('social.apps.pyramid_app') - - -Models Setup ------------- - -At the moment the models for python-social-auth_ are defined inside a function -because they need the reference to the current DB instance and the User model -used on your project (check *User model reference* below). Once the Pyramid -application configuration and database are defined, call ``init_social`` to -register the models:: - - from social.apps.pyramid_app.models import init_social - - init_social(config, Base, DBSession) - -So far I wasn't able to find another way to define the models on another way -rather than making it as a side-effect of calling this function since the -database is not available and ``current_app`` cannot be used on initialization -time, just run time. - - -User model reference --------------------- - -The application keeps a reference to the User model used by your project, -define it by using this setting:: - - SOCIAL_AUTH_USER_MODEL = 'foobar.models.User' - -The value must be the import path to the User model. - - -Global user ------------ - -The application expects the current logged in user accessible at ``request.user``, -the example application ensures that with this hander:: - - def get_user(request): - user_id = request.session.get('user_id') - if user_id: - user = DBSession.query(User)\ - .filter(User.id == user_id)\ - .first() - else: - user = None - return user - -The handler is added to the configuration doing:: - - config.add_request_method('example.auth.get_user', 'user', reify=True) - -This is just a simple example, probably your project does it in a better way. - - -User login ----------- - -Since the application doesn't make any assumption on how you are going to login -the users, you need to specify it. In order to do that, define these settings:: - - SOCIAL_AUTH_LOGIN_FUNCTION = 'example.auth.login_user' - SOCIAL_AUTH_LOGGEDIN_FUNCTION = 'example.auth.login_required' - -The first one must accept the strategy used and the user instance that was -created or retrieved from the database, there you can set the user id in the -session or cookies or whatever place used later to retrieve the id again and -load the user from the database (check the snippet above in *Global User*). - -The second one is used to ensure that there's a user logged in when calling the -disconnect view. It must accept a ``User`` instance and return ``True`` or -``Flase``. - -Check the auth.py_ in the example application for details on how it's done -there. - - -Social auth in templates context --------------------------------- - -To access the social instances related to a user in the template context, you -can do so by accessing the ``social_auth`` attribute in the user instance:: - -
        • ${social.provider}
        • - -Also you can add the backends (associated and not associated to a user) by -enabling this context function in your project:: - - from pyramid.events import subscriber, BeforeRender - from social.apps.pyramid_app.utils import backends - - @subscriber(BeforeRender) - def add_social(event): - request = event['request'] - event.update(backends(request, request.user)) - -That will load a dict with entries:: - - { - 'associated': [...], - 'not_associated': [...], - 'backends': [...] - } - -The ``associated`` key will have all the associated ``UserSocialAuth`` -instances related to the given user. ``not_associated`` will have the backends -names not associated and backends will have all the enabled backends names. - - -.. _Pyramid: http://www.pylonsproject.org/projects/pyramid/about -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _Pyramid built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/pyramid_app -.. _sqlalchemy: http://www.sqlalchemy.org/ -.. _auth.py: https://github.com/omab/python-social-auth/blob/master/examples/pyramid_example/example/auth.py diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst deleted file mode 100644 index efff5845c..000000000 --- a/docs/configuration/settings.rst +++ /dev/null @@ -1,311 +0,0 @@ -Configuration -============= - -Application setup ------------------ - -Once the application is installed (check Installation_) define the following -settings to enable the application behavior. Also check the sections dedicated -to each framework for detailed instructions. - - -Settings name -------------- - -Almost all settings are prefixed with ``SOCIAL_AUTH_``, there are some -exceptions for Django framework like ``AUTHENTICATION_BACKENDS``. - -All settings can be defined per-backend by adding the backend name to the -setting name like ``SOCIAL_AUTH_TWITTER_LOGIN_URL``. Settings discovery is done -by reducing the name starting with backend setting, then app setting and -finally global setting, for example:: - - SOCIAL_AUTH_TWITTER_LOGIN_URL - SOCIAL_AUTH_LOGIN_URL - LOGIN_URL - -The backend name is generated from the ``name`` attribute from the backend -class by uppercasing it and replacing ``-`` with ``_``. - - -Keys and secrets ----------------- - -- Setup needed OAuth keys (see OAuth_ section for details):: - - SOCIAL_AUTH_TWITTER_KEY = 'foobar' - SOCIAL_AUTH_TWITTER_SECRET = 'bazqux' - -OpenId backends don't require keys usually, but some need some API Key to -call any API on the provider. Check Backends_ sections for details. - - -Authentication backends ------------------------ - -Register the backends you plan to use, on Django framework use the usual -``AUTHENTICATION_BACKENDS`` settings, for others, define -``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``:: - - SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - ... - ) - - -URLs options ------------- - -These URLs are used on different steps of the auth process, some for successful -results and others for error situations. - -``SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/logged-in/'`` - Used to redirect the user once the auth process ended successfully. The - value of ``?next=/foo`` is used if it was present - -``SOCIAL_AUTH_LOGIN_ERROR_URL = '/login-error/'`` - URL where the user will be redirected in case of an error - -``SOCIAL_AUTH_LOGIN_URL = '/login-url/'`` - Is used as a fallback for ``LOGIN_ERROR_URL`` - -``SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/new-users-redirect-url/'`` - Used to redirect new registered users, will be used in place of - ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` if defined. Note that ``?next=/foo`` is appended if present, - if you want new users to go to next, you'll need to do it yourself. - -``SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/new-association-redirect-url/'`` - Like ``SOCIAL_AUTH_NEW_USER_REDIRECT_URL`` but for new associated accounts - (user is already logged in). Used in place of ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` - -``SOCIAL_AUTH_DISCONNECT_REDIRECT_URL = '/account-disconnected-redirect-url/'`` - The user will be redirected to this URL when a social account is - disconnected - -``SOCIAL_AUTH_INACTIVE_USER_URL = '/inactive-user/'`` - Inactive users can be redirected to this URL when trying to authenticate. - -Successful URLs will default to ``SOCIAL_AUTH_LOGIN_URL`` while error URLs will -fallback to ``SOCIAL_AUTH_LOGIN_ERROR_URL``. - - -User model ----------- - -``UserSocialAuth`` instances keep a reference to the ``User`` model of your -project, since this is not known, the ``User`` model must be configured by -a setting:: - - SOCIAL_AUTH_USER_MODEL = 'foo.bar.User' - -``User`` model must have a ``username`` and ``email`` field, these are -required. - -Also an ``is_authenticated`` and ``is_active`` boolean flags are recommended, -these can be methods if necessary (must return ``True`` or ``False``). If the -model lacks them a ``True`` value is assumed. - - -Tweaking some fields length ---------------------------- - -Some databases impose limitations on index columns (like MySQL InnoDB). These -limitations won't play nice on some ``UserSocialAuth`` fields. To avoid such -errors, define some of the following settings. - -``SOCIAL_AUTH_UID_LENGTH = `` - Used to define the max length of the field `uid`. A value of 223 should work - when using MySQL InnoDB which impose a 767 bytes limit (assuming UTF-8 - encoding). - -``SOCIAL_AUTH_NONCE_SERVER_URL_LENGTH = `` - ``Nonce`` model has a unique constraint over ``('server_url', 'timestamp', - 'salt')``, salt has a max length of 40, so ``server_url`` length must be - tweaked using this setting. - -``SOCIAL_AUTH_ASSOCIATION_SERVER_URL_LENGTH = `` or ``SOCIAL_AUTH_ASSOCIATION_HANDLE_LENGTH = `` - ``Association`` model has a unique constraint over ``('server_url', - 'handle')``, both fields lengths can be tweaked by these settings. - - -Username generation -------------------- - -Some providers return a username, others just an ID or email or first and last -names. The application tries to build a meaningful username when possible but -defaults to generating one if needed. - -A UUID is appended to usernames in case of collisions. Here are some settings -to control username generation. - -``SOCIAL_AUTH_UUID_LENGTH = 16`` - This controls the length of the UUID appended to usernames. - -``SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = True`` - If you want to use the full email address as the ``username``, define this - setting. - -``SOCIAL_AUTH_SLUGIFY_USERNAMES = False`` - For those that prefer slugged usernames, the ``get_username`` pipeline can - apply a slug transformation (code borrowed from Django project) by defining - this setting to ``True``. The feature is disabled by default to to not - force this option to all projects. - -``SOCIAL_AUTH_CLEAN_USERNAMES = True`` - By default the regex ``r'[^\w.@+-_]+'`` is applied over usernames to clean - them from usual undesired characters like spaces. Set this setting to - ``False`` to disable this behavior. - - -Extra arguments on auth processes ---------------------------------- - -Some providers accept particular GET parameters that produce different results -during the auth process, usually used to show different dialog types (mobile -version, etc). - -You can send extra parameters on auth process by defining settings per backend, -example to request Facebook to show Mobile authorization page, define:: - - FACEBOOK_AUTH_EXTRA_ARGUMENTS = {'display': 'touch'} - -For other providers, just define settings in the form:: - - SOCIAL_AUTH__AUTH_EXTRA_ARGUMENTS = {...} - -Also, you can send extra parameters on request token process by defining -settings in the same way explained above but with this other suffix:: - - SOCIAL_AUTH__REQUEST_TOKEN_EXTRA_ARGUMENTS = {...} - -Basic information is requested to the different providers in order to create -a coherent user instance (with first and last name, email and full name), this -could be too intrusive for some sites that want to ask users the minimum data -possible. It's possible to override the default values requested by defining -any of the following settings, for Open Id providers:: - - SOCIAL_AUTH__IGNORE_DEFAULT_AX_ATTRS = True - SOCIAL_AUTH__AX_SCHEMA_ATTRS = [ - (schema, alias) - ] - -For OAuth backends:: - - SOCIAL_AUTH__IGNORE_DEFAULT_SCOPE = True - SOCIAL_AUTH__SCOPE = [ - ... - ] - - -Processing redirects and urlopen --------------------------------- - -The application issues several redirects and API calls. The following settings -allow some tweaks to the behavior of these. - -``SOCIAL_AUTH_SANITIZE_REDIRECTS = False`` - The auth process finishes with a redirect, by default it's done to the - value of ``SOCIAL_AUTH_LOGIN_REDIRECT_URL`` but can be overridden with - ``next`` GET argument. If this setting is ``True``, this application will - vary the domain of the final URL and only redirect to it if it's on the - same domain. - -``SOCIAL_AUTH_REDIRECT_IS_HTTPS = False`` - On projects behind a reverse proxy that uses HTTPS, the redirect URIs - can have the wrong schema (``http://`` instead of ``https://``) if - the request lacks the appropriate headers, which might cause errors during - the auth process. To force HTTPS in the final URIs set this setting to - ``True`` - -``SOCIAL_AUTH_URLOPEN_TIMEOUT = 30`` - Any ``urllib2.urlopen`` call will be performed with the default timeout - value, to change it without affecting the global socket timeout define this - setting (the value specifies timeout seconds). - - ``urllib2.urlopen`` uses ``socket.getdefaulttimeout()`` value by default, so - setting ``socket.setdefaulttimeout(...)`` will affect ``urlopen`` when this - setting is not defined, otherwise this setting takes precedence. Also this - might affect other places in Django. - - ``timeout`` argument was introduced in python 2.6 according to `urllib2 - documentation`_ - - -Whitelists ----------- - -Registration can be limited to a set of users identified by their email -address or domain name. To white-list just set any of these settings: - -``SOCIAL_AUTH__WHITELISTED_DOMAINS = ['foo.com', 'bar.com']`` - Supply a list of domain names to be white-listed. Any user with an email - address on any of the allowed domains will login successfully, otherwise - ``AuthForbidden`` is raised. - -``SOCIAL_AUTH__WHITELISTED_EMAILS = ['me@foo.com', 'you@bar.com']`` - Supply a list of email addresses to be white-listed. Any user with an email - address in this list will login successfully, otherwise ``AuthForbidden`` - is raised. - - -Miscellaneous settings ----------------------- - -``SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email',]`` - During the pipeline process a ``dict`` named ``details`` will be populated - with the needed values to create the user instance, but it's also used to - update the user instance. Any value in it will be checked as an attribute - in the user instance (first by doing ``hasattr(user, name)``). Usually - there are attributes that cannot be updated (like ``username``, ``id``, - ``email``, etc.), those fields need to be *protect*. Set any field name that - requires *protection* in this setting, and it won't be updated. - -``SOCIAL_AUTH_SESSION_EXPIRATION = False`` - By default, user session expiration time will be set by your web - framework (in Django, for example, it is set with - `SESSION_COOKIE_AGE`_). Some providers return the time that the - access token will live, which is stored in ``UserSocialAuth.extra_data`` - under the key ``expires``. Changing this setting to True will override your - web framework's session length setting and set user session lengths to - match the ``expires`` value from the auth provider. - -``SOCIAL_AUTH_OPENID_PAPE_MAX_AUTH_AGE = `` - Enable `OpenID PAPE`_ extension support by defining this setting. - -``SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['foo',]`` - If you want to store extra parameters from POST or GET in session, like it - was made for ``next`` parameter, define this setting with the parameter - names. - - In this case ``foo`` field's value will be stored when user follows this - link ``...``. - -``SOCIAL_AUTH_PASSWORDLESS = False`` - When this setting is ``True`` and ``social.pipeline.mail.send_validation`` - is enabled, it allows the implementation of a `passwordless authentication - mechanism`_. Example of this implementation can be found at - psa-passwordless_. - - -Account disconnection ---------------------- - -Disconnect is an side-effect operation and should be done by POST method only, -some CSRF protection is encouraged (and enforced on Django app). Ensure that -any call to `/disconnect//` or `/disconnect///` is done -using POST. - - -.. _urllib2 documentation: http://docs.python.org/library/urllib2.html#urllib2.urlopen -.. _OpenID PAPE: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html -.. _Installation: ../installing.html -.. _Backends: ../backends/index.html -.. _OAuth: http://oauth.net/ -.. _passwordless authentication mechanism: https://medium.com/@ninjudd/passwords-are-obsolete-9ed56d483eb -.. _psa-passwordless: https://github.com/omab/psa-passwordless -.. _SESSION_COOKIE_AGE: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-SESSION_COOKIE_AGE diff --git a/docs/configuration/webpy.rst b/docs/configuration/webpy.rst deleted file mode 100644 index 8e1ada471..000000000 --- a/docs/configuration/webpy.rst +++ /dev/null @@ -1,74 +0,0 @@ -Webpy Framework -=============== - -Webpy_ framework is easy to setup, once that python-social-auth_ is installed -or accessible in the ``PYTHONPATH``, just add the needed configurations to make -it run. - - -Dependencies ------------- - -The `Webpy built-in app` depends on sqlalchemy_, there's no support for others -ORMs yet but pull-requests are welcome. - - -Configuration -------------- - -Add the needed settings into ``web.config`` store. Settings are prefixed with -``SOCIAL_AUTH_`` but there's a helper for it:: - - from social.utils import setting_name - - web.config[setting_name('USER_MODEL')] = 'models.User' - web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/' - web.config[setting_name('AUTHENTICATION_BACKENDS')] = ( - 'social.backends.google.GoogleOAuth2', - ... - ) - -Add all the settings needed for the app (check Configuration_ section for -details). - - -URLs ----- - -Add the social application into URLs:: - - from social.apps.webpy_app import app as social_app - - urls = ( - ... - '', social_app.app_social - ... - ) - - -Session -------- - -python-social-auth_ depends on sessions storage to keep some essential values, -usually redirects and ``state`` parameters used to validate authentication -process on OAuth providers. - -The `Webpy built-in app` expects the session reference to be available under -``web.web_session`` so ensure it's available there. - - -User model ----------- - -Like the other apps, the User model must be defined on settings since -a reference to it is kept on ``UserSocialAuth`` instance. Define like this:: - - web.config[setting_name('USER_MODEL')] = 'models.User' - -Where the value is the import path to the User model used on your project. - - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _Webpy: http://webpy.org/ -.. _Webpy built-in app: https://github.com/omab/python-social-auth/tree/master/social/apps/webpy_app -.. _sqlalchemy: http://www.sqlalchemy.org/ diff --git a/docs/copyright.rst b/docs/copyright.rst deleted file mode 100644 index 32bf55bc0..000000000 --- a/docs/copyright.rst +++ /dev/null @@ -1,12 +0,0 @@ -Copyrights and Licence -====================== - -``python-social-auth`` is protected by BSD licence. Check the LICENCE_ for -details. - -The base work was derived from django-social-auth_ work and copyrighted too, -check `django-social-auth LICENCE`_ for details: - -.. _LICENCE: https://github.com/omab/python-social-auth/blob/master/LICENSE -.. _django-social-auth: https://github.com/omab/django-social-auth -.. _django-social-auth LICENCE: https://github.com/omab/django-social-auth/blob/master/LICENSE diff --git a/docs/developer_intro.rst b/docs/developer_intro.rst deleted file mode 100644 index ebd8526cc..000000000 --- a/docs/developer_intro.rst +++ /dev/null @@ -1,170 +0,0 @@ -Beginners Guide -=============== - -This is an attempt to bring together a number of concepts in python-social-auth -(psa) so that you will understand how it fits into your system. This definitely -has a Django flavor to it (because that's how I learned it). - -Understanding PSA URLs ------------------------ - -If you have not seen namespaced URLs before, you are about to be introduced. -When you add the PSA entry to your urls.py, it looks like this:: - - url(r'', include('social.apps.django_app.urls', namespace='social')) - -that "namespace" part on the end is what keeps the names in the PSA-world from -colliding with the names in your app, or other 3rd-party apps. So your login -link will look like this:: - - Login - -(See how "social" in the URL mapping matches the value of "namespace" in the -urls.py entry?) - -Understanding Backends ----------------------- - -PSA implements a lot of backends. Find the entry in the docs for your backend, -and if it's there, follow the steps to enable it, which come down to - -1) Set up SOCIAL_AUTH_{backend} variables in settings.py. (The - settings vary, based on the backends) - -2) Adding your backend to AUTHENTICATION_BACKENDS in settings.py. - -If you need to implement a different backend (for instance, let's say you -want to use Intuit's OpenID), you can subclass the nearest one and override -the "name" attribute:: - - from social.backends.open_id import OpenIDAuth - - class IntuitOpenID(OpenIDAuth): - name = 'intuit' - -And then add your new backend to AUTHENTICATION_BACKENDS in settings.py. - -A couple notes about the pipeline: - -The standard pipeline does not log the user in until after the pipeline has -completed. So if you get a value in the user key of the accumulative -dictionary, that implies that the user was logged in when the process started. - -Understanding the Pipeline --------------------------- - -Reversing a URL like ``{% url 'social:begin' 'github' %}`` will give you a url -like:: - - http://example.com/login/github - -And clicking on that link will cause the "pipeline" to be started. The pipeline -is a list of functions that build up data about the user as we go through the -steps of the authentication process. (If you really want to understand the -pipeline, look at the source in ``social/backends/base.py``, and see the -``run_pipeline()`` function in ``BaseAuth``.) - -The design contract for each function in the pipeline is: - -1) The pipeline starts with a four-item dictionary (the accumulative dictionary) - which is updated with the results of each function in the pipeline. The - initial four values are: - - ``strategy`` - contains a strategy object - ``backend`` - contains the backend being used during this pipeline run - ``request`` - contains a dictionary of the request keys. Note to Django users -- this is - not an HttpRequest object, it is actually the results of - ``request.REQUEST``. - ``details`` - which is an empty dict. - -2) If the function returns a dictionary or something False-ish, add the contents - of the dictionary to an accumulative dictionary (called ``out`` in - ``run_pipeline``), and call the next step in the pipeline with the - accumulative dictionary. - -3) If something else is returned (for example, a subclass of ``HttpResponse``), - then return that to the browser. - -4) If the pipeline completes, *THEN* the user is authenticated (logged in). So - if you are finding an authenticated user object while the pipeline is - running, that means that the user was logged in when the pipeline started. - -There is one pipeline for your site as a whole -- if you have backend-specific -logic, you have to make your pipeline steps smart enough to skip the step if it -is not relevant. This is as simple as:: - - def my_custom_step(strategy, backend, request, details, *args, **kwargs): - if backend_name != 'my_custom_backend': - return - # otherwise, do the special steps for your custom backend - -Interrupting the Pipeline (and communicating with views) ---------------------------------------------------------- - -Let's say you want to add a custom step in the pipeline -- you want the user -to establish a password so that they can come directly to your site in the -future. We can do that with the @partial decorator, which tells the pipeline -to keep track of where it is so that it can be restarted. - -The first thing we need to do is set up a way for our views to communicate with -the pipeline. That is done by adding a value to the settings file to tell -us which values should be passed back and forth between the Django session -and the pipeline:: - - SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['local_password',] - -In our pipeline code, we would have:: - - from django.shortcuts import redirect - from django.contrib.auth.models import User - from social.pipeline.partial import partial - - # partial says "we may interrupt, but we will come back here again" - @partial - def collect_password(strategy, backend, request, details, *args, **kwargs): - # request['local_password'] is set by the pipeline infrastructure - # because it exists in FIELDS_STORED_IN_SESSION - if not request.get('local_password', None): - - # if we return something besides a dict or None, then that is - # returned to the user -- in this case we will redirect to a - # view that can be used to get a password - return redirect("myapp.views.collect_password") - - # grab the user object from the database (remember that they may - # not be logged in yet) and set their password. (Assumes that the - # email address was captured in an earlier step.) - user = User.objects.get(email=kwargs['email']) - user.set_password(request['local_password']) - user.save() - - # continue the pipeline - return - -In our view code, we would have something like:: - - class PasswordForm(forms.Form): - secret_word = forms.CharField(max_length=10) - - def get_user_password(request): - if request.method == 'POST': - form = PasswordForm(request.POST) - if form.is_valid(): - # because of FIELDS_STORED_IN_SESSION, this will get copied - # to the request dictionary when the pipeline is resumed - request.session['local_password'] = form.cleaned_data['secret_word'] - - # once we have the password stashed in the session, we can - # tell the pipeline to resume by using the "complete" endpoint - return redirect(reverse('social:complete', args=("backend_name,"))) - else: - form = PasswordForm() - - return render(request, "password_form.html") - -Note that the ``social:complete`` will re-enter the pipeline with the same -function that interrupted it (in this case, collect_password). diff --git a/docs/exceptions.rst b/docs/exceptions.rst deleted file mode 100644 index fdbfa17c8..000000000 --- a/docs/exceptions.rst +++ /dev/null @@ -1,55 +0,0 @@ -Exceptions -========== - -This set of exceptions were introduced to describe the situations a bit more -than just the ``ValueError`` usually raised. - -``SocialAuthBaseException`` - Base class for all social auth exceptions. - -``AuthException`` - Base exception class for authentication process errors. - -``AuthFailed`` - Authentication failed for some reason. - -``AuthCanceled`` - Authentication was canceled by the user. - -``AuthUnknownError`` - An unknown error stoped the authentication process. - -``AuthTokenError`` - Unauthorized or access token error, it was invalid, impossible to - authenticate or user removed permissions to it. - -``AuthMissingParameter`` - A needed parameter to continue the process was missing, usually raised by - the services that need some POST data like myOpenID. - -``AuthAlreadyAssociated`` - A different user has already associated the social account that the current - user is trying to associate. - -``WrongBackend`` - Raised when the backend given in the URLs is invalid (not enabled or - registered). - -``NotAllowedToDisconnect`` - Raised on disconnect action when it's not safe for the user to disconnect - the social account, probably because the user lacks a password or another - social account. - -``AuthStateMissing`` - The state parameter is missing from the server response. - -``AuthStateForbidden`` - The state parameter returned by the server is not the one sent. - -``AuthTokenRevoked`` - Raised when the user revoked the access_token in the provider. - -``AuthUnreachableProvider`` - Raised when server couldn't communicate with backend. - -These are a subclass of ``ValueError`` to keep backward compatibility. diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 473de706b..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,46 +0,0 @@ -Welcome to Python Social Auth's documentation! -============================================== - -Python Social Auth aims to be an easy to setup social authentication and -authorization mechanism for Python projects supporting protocols like OAuth (1 -and 2), OpenId and others. - -The initial codebase is derived from django-social-auth_ with the idea of -generalizing the process to suit the different frameworks around, providing -the needed tools to bring support to new frameworks. - -django-social-auth_ itself was a product of modified code from -django-twitter-oauth_ and django-openid-auth_ projects. - - -Contents: - -.. toctree:: - :maxdepth: 2 - - intro - installing - configuration/index - pipeline - strategies - storage - exceptions - backends/index - developer_intro - logging_out - tests - use_cases - thanks - copyright - - -Indices and Tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - -.. _django-social-auth: http://github.com/omab/django-social-auth -.. _django-twitter-oauth: https://github.com/henriklied/django-twitter-oauth -.. _django-openid-auth: https://launchpad.net/django-openid-auth diff --git a/docs/installing.rst b/docs/installing.rst deleted file mode 100644 index c1f0cb426..000000000 --- a/docs/installing.rst +++ /dev/null @@ -1,58 +0,0 @@ -Installation -============ - -Dependencies ------------- - -Dependencies that **must** be met to use the application: - -- OpenId_ support depends on python-openid_ - -- OAuth_ support depends on requests-oauthlib_ - -- Several backends demands application registration on their corresponding - sites and other dependencies like sqlalchemy_ on Flask and Webpy. - - -Get a copy ----------- - -From pypi_:: - - $ pip install python-social-auth - -Or:: - - $ easy_install python-social-auth - -Or clone from github_:: - - $ git clone git://github.com/omab/python-social-auth.git - -And add social to ``PYTHONPATH``:: - - $ export PYTHONPATH=$PYTHONPATH:$(pwd)/python-social-auth/ - -Or:: - - $ cd python-social-auth - $ sudo python setup.py install - - -.. _OpenId: http://openid.net/ -.. _OAuth: http://oauth.net/ -.. _pypi: http://pypi.python.org/pypi/python-social-auth/ -.. _github: https://github.com/omab/python-social-auth -.. _python-openid: http://pypi.python.org/pypi/python-openid/ -.. _requests-oauthlib: https://requests-oauthlib.readthedocs.org/ -.. _sqlalchemy: http://www.sqlalchemy.org/ - -Upgrading ---------- - -Django with South -~~~~~~~~~~~~~~~~~ - -Upgrading from 0.1 to 0.2 is likely to cause problems trying to apply a migration when the tables already exist. In this case a fake migration needs to be applied: - -$ python manage.py migrate --fake default diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index af3e37c2e..000000000 --- a/docs/intro.rst +++ /dev/null @@ -1,183 +0,0 @@ -Introduction -============ - -Python Social Auth aims to be an easy to setup social authentication and -authorization mechanism for Python projects supporting protocols like OAuth_ (1 -and 2), OpenId_ and others. - - -Features --------- - -This application provides user registration and login using social sites -credentials, here are some features, probably not a full list yet. - - -Supported frameworks -******************** - -Multiple frameworks support: - - * Django_ - * Flask_ - * Pyramid_ - * Webpy_ - * Tornado_ - -More frameworks can be added easily (and should be even easier in the future -once the code matures). - - -Auth providers -************** - -Several supported service by simple backends definition (easy to add new ones -or extend current one): - - * Angel_ OAuth2 - * Beats_ OAuth2 - * Behance_ OAuth2 - * Bitbucket_ OAuth1 - * Box_ OAuth2 - * Dailymotion_ OAuth2 - * Deezer_ OAuth2 - * Disqus_ OAuth2 - * Douban_ OAuth1 and OAuth2 - * Dropbox_ OAuth1 - * Evernote_ OAuth1 - * Facebook_ OAuth2 and OAuth2 for Applications - * Fitbit_ OAuth2 and OAuth1 - * Flickr_ OAuth1 - * Foursquare_ OAuth2 - * `Google App Engine`_ Auth - * Github_ OAuth2 - * Google_ OAuth1, OAuth2 and OpenId - * Instagram_ OAuth2 - * Kakao_ OAuth2 - * Linkedin_ OAuth1 - * Live_ OAuth2 - * Livejournal_ OpenId - * Mailru_ OAuth2 - * MineID_ OAuth2 - * Mixcloud_ OAuth2 - * `Mozilla Persona`_ - * NaszaKlasa_ OAuth2 - * `NGPVAN ActionID`_ OpenId - * Odnoklassniki_ OAuth2 and Application Auth - * OpenId_ - * Podio_ OAuth2 - * Pinterest_ OAuth2 - * Rdio_ OAuth1 and OAuth2 - * Readability_ OAuth1 - * Shopify_ OAuth2 - * Skyrock_ OAuth1 - * Soundcloud_ OAuth2 - * Spotify_ OAuth2 - * ThisIsMyJam_ OAuth1 - * Stackoverflow_ OAuth2 - * Steam_ OpenId - * Stocktwits_ OAuth2 - * Stripe_ OAuth2 - * Tripit_ OAuth1 - * Tumblr_ OAuth1 - * Twilio_ Auth - * Twitch_ OAuth2 - * Twitter_ OAuth1 - * Upwork_ OAuth1 - * Vimeo_ OAuth1 - * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications - * Weibo_ OAuth2 - * Wunderlist_ OAuth2 - * Xing_ OAuth1 - * Yahoo_ OpenId and OAuth1 - * Yammer_ OAuth2 - * Yandex_ OAuth1, OAuth2 and OpenId - - -User data -********* - -Basic user data population, to allow custom fields values from providers -response. - - -Social accounts association -*************************** - -Multiple social accounts can be associated to a single user. - - -Authentication and disconnection processing -******************************************* - -Extensible pipeline to handle authentication, association and disconnection -mechanism in ways that suits your project. Check `Authentication Pipeline`_ -section. - - -.. _OpenId: http://openid.net/ -.. _OAuth: http://oauth.net/ -.. _myOpenID: https://www.myopenid.com/ -.. _Angel: https://angel.co -.. _Beats: https://www.beats.com -.. _Behance: https://www.behance.net -.. _Bitbucket: https://bitbucket.org -.. _Box: https://www.box.com -.. _Dailymotion: https://dailymotion.com -.. _Deezer: https://www.deezer.com -.. _Disqus: https://disqus.com -.. _Douban: http://www.douban.com -.. _Dropbox: https://dropbox.com -.. _Evernote: https://www.evernote.com -.. _Facebook: https://www.facebook.com -.. _Fitbit: https://fitbit.com -.. _Flickr: http://www.flickr.com -.. _Foursquare: https://foursquare.com -.. _Google App Engine: https://developers.google.com/appengine/ -.. _Github: https://github.com -.. _Google: http://google.com -.. _Instagram: https://instagram.com -.. _Kakao: https://kakao.com -.. _Linkedin: https://www.linkedin.com -.. _Live: https://www.live.com -.. _Livejournal: http://livejournal.com -.. _Mailru: https://mail.ru -.. _MineID: https://www.mineid.org -.. _Mixcloud: https://www.mixcloud.com -.. _Mozilla Persona: http://www.mozilla.org/persona/ -.. _NaszaKlasa: https://developers.nk.pl/ -.. _NGPVAN ActionID: http://developers.ngpvan.com/action-id -.. _Odnoklassniki: http://www.odnoklassniki.ru -.. _Podio: https://podio.com -.. _Shopify: http://shopify.com -.. _Skyrock: https://skyrock.com -.. _Soundcloud: https://soundcloud.com -.. _Spotify: https://www.spotify.com -.. _ThisIsMyJam: https://thisismyjam.com -.. _Stocktwits: https://stocktwits.com -.. _Stripe: https://stripe.com -.. _Tripit: https://www.tripit.com -.. _Twilio: https://www.twilio.com -.. _Twitch: http://www.twitch.tv/ -.. _Twitter: http://twitter.com -.. _VK.com: http://vk.com -.. _Weibo: http://weibo.com -.. _Wunderlist: http://wunderlist.com -.. _Xing: https://www.xing.com -.. _Yahoo: http://yahoo.com -.. _Yammer: https://www.yammer.com -.. _Yandex: https://yandex.ru -.. _Pinterest: https://www.pinterest.com -.. _Readability: http://www.readability.com/ -.. _Stackoverflow: http://stackoverflow.com/ -.. _Steam: http://steamcommunity.com/ -.. _Rdio: https://www.rdio.com -.. _Vimeo: https://vimeo.com/ -.. _Tumblr: http://www.tumblr.com/ -.. _Django: https://github.com/omab/python-social-auth/tree/master/social/apps/django_app -.. _Flask: https://github.com/omab/python-social-auth/tree/master/social/apps/flask_app -.. _Pyramid: http://www.pylonsproject.org/projects/pyramid/about -.. _Webpy: https://github.com/omab/python-social-auth/tree/master/social/apps/webpy_app -.. _Tornado: http://www.tornadoweb.org/ -.. _Authentication Pipeline: pipeline.html -.. _Upwork: https://www.upwork.com diff --git a/docs/logging_out.rst b/docs/logging_out.rst deleted file mode 100644 index bd10bd86e..000000000 --- a/docs/logging_out.rst +++ /dev/null @@ -1,25 +0,0 @@ -Disconnect and Logging Out -========================== - -It's a common misconception that the ``disconnect`` action is the same as -logging the user out, but this is not the case. - -``Disconnect`` is the way that your users can ask your project to "forget about -my account". This implies removing the ``UserSocialAuth`` instance that was -created, this also implies that the user won't be able to login back into your -site with the social account. Instead the action will be a signup, a new user -instance will be created, not related to the previous one. - -Logging out is just a way to say "forget my current session", and usually -implies removing cookies, invalidating a session hash, etc. The many frameworks -have their own ways to logout an account (Django has ``django.contrib.auth.logout``), -``flask-login`` has it's own way too with `logout_user()`_. - -Since disconnecting a social account means that the user won't be able to log -back in with that social provider into the same user, python-social-auth will -check that the user account is in a valid state for disconnection (it has at -least one more social account associated, or a password, etc). This behavior -can be overridden by changing the `Disconnection Pipeline`_. - -.. _logout_user(): https://github.com/maxcountryman/flask-login/blob/a96de342eae560deec008a02179f593c3799b3ba/flask_login.py#L718-L739 -.. _Disconnection Pipeline: pipeline.html#disconnection-pipeline diff --git a/docs/pipeline.rst b/docs/pipeline.rst deleted file mode 100644 index b3efc486c..000000000 --- a/docs/pipeline.rst +++ /dev/null @@ -1,348 +0,0 @@ -Pipeline -======== - -python-social-auth_ uses an extendible pipeline mechanism where developers can -introduce their functions during the authentication, association and -disconnection flows. - -The functions will receive a variable set of arguments related to the current -process, common arguments are the current ``strategy``, ``user`` (if any) and -``request``. It's recommended that all the function also define an ``**kwargs`` -in the parameters to avoid errors for unexpected arguments. - -Each pipeline entry can return a ``dict`` or ``None``, any other type of return -value is treated as a response instance and returned directly to the client, -check *Partial Pipeline* below for details. - -If a ``dict`` is returned, the value in the set will be merged into the -``kwargs`` argument for the next pipeline entry, ``None`` is taken as if ``{}`` -was returned. - - -Authentication Pipeline ------------------------ - -The final process of the authentication workflow is handled by an operations -pipeline where custom functions can be added or default items can be removed to -provide a custom behavior. The default pipeline is a mechanism that creates -user instances and gathers basic data from providers. - -The default pipeline is composed by:: - - ( - # Get the information we can about the user and return it in a simple - # format to create the user instance later. On some cases the details are - # already part of the auth response from the provider, but sometimes this - # could hit a provider API. - 'social.pipeline.social_auth.social_details', - - # Get the social uid from whichever service we're authing thru. The uid is - # the unique identifier of the given user in the provider. - 'social.pipeline.social_auth.social_uid', - - # Verifies that the current auth process is valid within the current - # project, this is where emails and domains whitelists are applied (if - # defined). - 'social.pipeline.social_auth.auth_allowed', - - # Checks if the current social-account is already associated in the site. - 'social.pipeline.social_auth.social_user', - - # Make up a username for this person, appends a random string at the end if - # there's any collision. - 'social.pipeline.user.get_username', - - # Send a validation email to the user to verify its email address. - # Disabled by default. - # 'social.pipeline.mail.mail_validation', - - # Associates the current social details with another user account with - # a similar email address. Disabled by default. - # 'social.pipeline.social_auth.associate_by_email', - - # Create a user account if we haven't found one yet. - 'social.pipeline.user.create_user', - - # Create the record that associates the social account with the user. - 'social.pipeline.social_auth.associate_user', - - # Populate the extra_data field in the social record with the values - # specified by settings (and the default ones like access_token, etc). - 'social.pipeline.social_auth.load_extra_data', - - # Update the user record with any changed info from the auth service. - 'social.pipeline.user.user_details', - ) - - -It's possible to override it by defining the setting ``SOCIAL_AUTH_PIPELINE``. -For example, a pipeline that won't create users, just accept already registered -ones would look like this:: - - SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - ) - -Note that this assumes the user is already authenticated, and thus the ``user`` key -in the dict is populated. In cases where the authentication is purely external, a -pipeline method must be provided that populates the ``user`` key. Example:: - - - SOCIAL_AUTH_PIPELINE = ( - 'myapp.pipeline.load_user', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - ) - -Each pipeline function will receive the following parameters: - * Current strategy (which gives access to current store, backend and request) - * User ID given by authentication provider - * User details given by authentication provider - * ``is_new`` flag (initialized as ``False``) - * Any arguments passed to ``auth_complete`` backend method, default views - pass these arguments: - - * current logged in user (if it's logged in, otherwise ``None``) - * current request - - -Disconnection Pipeline ----------------------- - -Like the authentication pipeline, it's possible to define a disconnection -pipeline if needed. - -For example, this can be useful on sites where a user that disconnects all the -related social account is required to fill a password to ensure the -authentication process in the future. This can be accomplished by overriding -the default disconnection pipeline and setup a function that checks if the user -has a password, in case it doesn't a redirect to a fill-your-password form can -be returned and later continue the disconnection process, take into account -that disconnection ensures the POST method by default, a simple method to -ensure this, is to make your form POST to ``/disconnect/`` and set the needed -password in your pipeline function. Check *Partial Pipeline* below. - -In order to override the disconnection pipeline, just define the setting:: - - SOCIAL_AUTH_DISCONNECT_PIPELINE = ( - # Verifies that the social association can be disconnected from the current - # user (ensure that the user login mechanism is not compromised by this - # disconnection). - 'social.pipeline.disconnect.allowed_to_disconnect', - - # Collects the social associations to disconnect. - 'social.pipeline.disconnect.get_entries', - - # Revoke any access_token when possible. - 'social.pipeline.disconnect.revoke_tokens', - - # Removes the social associations. - 'social.pipeline.disconnect.disconnect', - ) - - -Partial Pipeline ----------------- - -It's possible to cut the pipeline process to return to the user asking for more -data and resume the process later. To accomplish this decorate the function -that will cut the process with the ``@partial`` decorator located at -``social/pipeline/partial.py``. - -The old ``social.pipeline.partial.save_status_to_session`` is now deprecated. - -When it's time to resume the process just redirect the user to ``/complete//`` -or ``/disconnect//`` view. The pipeline will resume in the same -function that cut the process. - -``@partial`` and ``save_status_to_session`` stores needed data into user session -under the key ``partial_pipeline``. To get the backend in order to redirect to -any social view, just do:: - - backend = session['partial_pipeline']['backend'] - -Check the `example applications`_ to check a basic usage. - - -Email validation ----------------- - -There's a pipeline to validate email addresses, but it relies a lot on your -project. - -The pipeline is at ``social.pipeline.mail.mail_validation`` and it's a partial -pipeline, it will return a redirect to a URL that you can use to tell the -users that an email validation was sent to them. If you want to mention the -email address you can get it from the session under the key ``email_validation_address``. - -In order to send the validation python-social-auth_ needs a function that will -take care of it, this function is defined by the developer with the setting -``SOCIAL_AUTH_EMAIL_VALIDATION_FUNCTION``. It should be an import path. This -function should take three arguments ``strategy``, ``backend`` and ``code``. -``code`` is a model instance used to validate the email address, it contains -three fields: - -``code = '...'`` - Holds an ``uuid.uuid4()`` value and it's the code used to identify the - validation process. - -``email = '...'`` - Email address trying to be validate. - -``verified = True / False`` - Flag marking if the email was verified or not. - -You should use the code in this instance to build the link for email -validation which should go to ``/complete/email?verification_code=``. If you are using -Django, you can do it with:: - - from django.core.urlresolvers import reverse - url = strategy.build_absolute_uri( - reverse('social:complete', args=(strategy.backend_name,)) - ) + '?verification_code=' + code.code - -On Flask:: - - from flask import url_for - url = url_for('social.complete', backend=strategy.backend_name, - _external=True) + '?verification_code=' + code - -This pipeline can be used globally with any backend if this setting is -defined:: - - SOCIAL_AUTH_FORCE_EMAIL_VALIDATION = True - -Or individually by defining the setting per backend basis like -``SOCIAL_AUTH_TWITTER_FORCE_EMAIL_VALIDATION = True``. - - -Extending the Pipeline -====================== - -The main purpose of the pipeline (either creation or deletion pipelines) is to -allow extensibility for developers. You can jump in the middle of it, do -changes to the data, create other models instances, ask users for extra data, -or even halt the whole process. - -Extending the pipeline implies: - - 1. Writing a function - 2. Locating the function in an accessible path - (accessible in the way that it can be imported) - 3. Overriding the default pipeline definition with one that includes - newly created function. - -The part of writing the function is quite simple. However please be careful -when placing your function in the pipeline definition, because order -does matter in this case! Ordering of functions in ``SOCIAL_AUTH_PIPELINE`` -will determine the value of arguments that each function will receive. -For example, adding your function after ``social.pipeline.user.create_user`` -ensures that your function will get the user instance (created or already existent) -instead of a ``None`` value. - -The pipeline functions will get quite a lot of arguments, ranging from the -backend in use, different model instances, server requests and provider -responses. To enumerate a few: - -``strategy`` - The current strategy instance. - -``backend`` - The current backend instance. - -``uid`` - User ID in the provider, this ``uid`` should identify the user in the - current provider. - -``response = {} or object()`` - The server user-details response, it depends on the protocol in use (and - sometimes the provider implementation of such protocol), but usually it's - just a ``dict`` with the user profile details in such provider. Lots of - information related to the user is provided here, sometimes the ``scope`` - will increase the amount of information in this response on OAuth - providers. - -``details = {}`` - Basic user details generated by the backend, used to create/update the user - model details (this ``dict`` will contain values like ``username``, - ``email``, ``first_name``, ``last_name`` and ``fullname``). - -``user = None`` - The user instance (or ``None`` if it wasn't created or retrieved from the - database yet). - -``social = None`` - This is the associated ``UserSocialAuth`` instance for the given user (or - ``None`` if it wasn't created or retrieved from the DB yet). - -Usually when writing your custom pipeline function, you just want to get some -values from the ``response`` parameter. But you can do even more, like call -other APIs endpoints to retrieve even more details about the user, store them -on some other place, etc. - -Here's an example of a simple pipeline function that will create a ``Profile`` -class instance, related to the current user. This profile will store some simple details -returned by the provider (``Facebook`` in this example). The usual Facebook -``response`` looks like this:: - - { - 'username': 'foobar', - 'access_token': 'CAAD...', - 'first_name': 'Foo', - 'last_name': 'Bar', - 'verified': True, - 'name': 'Foo Bar', - 'locale': 'en_US', - 'gender': 'male', - 'expires': '5183999', - 'email': 'foo@bar.com', - 'updated_time': '2014-01-14T15:58:35+0000', - 'link': 'https://www.facebook.com/foobar', - 'timezone': -3, - 'id': '100000126636010', - } - -Let's say we are interested in storing the user profile link, the gender and -the timezone in our ``Profile`` model:: - - def save_profile(backend, user, response, *args, **kwargs): - if backend.name == 'facebook': - profile = user.get_profile() - if profile is None: - profile = Profile(user_id=user.id) - profile.gender = response.get('gender') - profile.link = response.get('link') - profile.timezone = response.get('timezone') - profile.save() - -Now all that's needed is to tell ``python-social-auth`` to use our function in -the pipeline. Since the function uses user instance, we need to put it after -``social.pipeline.user.create_user``:: - - SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', - 'social.pipeline.user.create_user', - 'path.to.save_profile', # <--- set the path to the function - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - ) - -So far the function we created returns ``None``, which is taken as if ``{}`` was returned. -If you want the ``profile`` object to be available to the next function in the -pipeline, all you need to do is return ``{'profile': profile}``. - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _example applications: https://github.com/omab/python-social-auth/tree/master/examples diff --git a/docs/storage.rst b/docs/storage.rst deleted file mode 100644 index e12e13bce..000000000 --- a/docs/storage.rst +++ /dev/null @@ -1,200 +0,0 @@ -Storage -======= - -Different frameworks support different ORMs, Storage solves the different -interfaces moving the common API to mixins classes. These mixins are used on -apps when defining the different models used by ``python-social-auth``. - - -Social User ------------ - -This model associates a social account data with a user in the system, it -contains the provider name and user ID (``uid``) which should identify the -social account in the remote provider, plus some extra data (``extra_data``) -which is JSON encoded field with extra information from the provider (usually -avatars and similar). - -When implementing this model, it must inherits from UserMixin_ and extend the -needed methods: - -* Username:: - - @classmethod - def get_username(cls, user): - """Return the username for given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def username_max_length(cls): - """Return the max length for username""" - raise NotImplementedError('Implement in subclass') - -* User model:: - - @classmethod - def user_model(cls): - """Return the user model""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def changed(cls, user): - """The given user instance is ready to be saved""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def user_exists(cls, username): - """ - Return True/False if a User instance exists with the given arguments. - Arguments are directly passed to filter() manager method. - """ - raise NotImplementedError('Implement in subclass') - - @classmethod - def create_user(cls, username, email=None): - """Create a user with given username and (optional) email""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_user(cls, pk): - """Return user instance for given id""" - raise NotImplementedError('Implement in subclass') - -* Social user:: - - @classmethod - def get_social_auth(cls, provider, uid): - """Return UserSocialAuth for given provider and uid""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get_social_auth_for_user(cls, user): - """Return all the UserSocialAuth instances for given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def create_social_auth(cls, user, uid, provider): - """Create a UserSocialAuth instance for given user""" - raise NotImplementedError('Implement in subclass') - -* Social disconnection:: - - @classmethod - def allowed_to_disconnect(cls, user, backend_name, association_id=None): - """Return if it's safe to disconnect the social account for the - given user""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def disconnect(cls, name, user, association_id=None): - """Disconnect the social account for the given user""" - raise NotImplementedError('Implement in subclass') - - -Nonce ------ - -This is a helper class for OpenId mechanism, it stores a one-use number, -shouldn't be used by the project since it's for internal use only. - -When implementing this model, it must inherits from NonceMixin_, and override -the needed method:: - - @classmethod - def use(cls, server_url, timestamp, salt): - """Create a Nonce instance""" - raise NotImplementedError('Implement in subclass') - - -Association ------------ - -Another OpenId helper class, it stores basic data to keep the OpenId -association. Like Nonce_ this is for internal use only. - -When implementing this model, it must inherits from AssociationMixin_, and -override the needed methods:: - - @classmethod - def store(cls, server_url, association): - """Create an Association instance""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def get(cls, *args, **kwargs): - """Get an Association instance""" - raise NotImplementedError('Implement in subclass') - - @classmethod - def remove(cls, ids_to_delete): - """Remove an Association instance""" - raise NotImplementedError('Implement in subclass') - - -Validation code ---------------- - -This class is used to keep track of email validations codes following the usual -email validation mechanism of sending an email to the user with a unique code. -This model is used by the partial pipeline ``social.pipeline.mail.mail_validation``. -Check the docs at *Email validation* in `pipeline docs`_. - -When implementing the model for your framework only one method needs to be -overridden:: - - @classmethod - def get_code(cls, code): - """Return the Code instance with the given code value""" - raise NotImplementedError('Implement in subclass') - - -Storage interface ------------------ - -There's a helper class used by strategies to hide the real models names under -a common API, an instance of this class is used by strategies to access the -storage modules. - -When implementing this class it must inherits from BaseStorage_, add the needed -models references and implement the needed method:: - - class StorageImplementation(BaseStorage): - user = UserModel - nonce = NonceModel - association = AssociationModel - code = CodeModel - - @classmethod - def is_integrity_error(cls, exception): - """Check if given exception flags an integrity error in the DB""" - raise NotImplementedError('Implement in subclass') - - -SQLAlchemy and Django mixins ----------------------------- - -Currently there are partial implementations of mixins for `SQLAlchemy ORM`_ and -`Django ORM`_ with common code used later on current implemented applications. - -**When using `SQLAlchemy ORM`_ and ``ZopeTransactionExtension``, it's -recommended to use the transaction_ application to handle them.** - -Models Examples ---------------- - -Check for current implementations for `Django App`_, `Flask App`_, `Pyramid -App`_, and `Webpy App`_ for examples of implementations. - - -.. _UserMixin: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L15 -.. _NonceMixin: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L149 -.. _AssociationMixin: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L161 -.. _BaseStorage: https://github.com/omab/python-social-auth/blob/master/social/storage/base.py#L201 -.. _SQLAlchemy ORM: https://github.com/omab/python-social-auth/blob/master/social/storage/sqlalchemy_orm.py -.. _Django ORM: https://github.com/omab/python-social-auth/blob/master/social/storage/django_orm.py -.. _Django App: https://github.com/omab/python-social-auth/blob/master/social/apps/django_app/default/models.py -.. _Flask App: https://github.com/omab/python-social-auth/blob/master/social/apps/flask_app/models.py -.. _Pyramid App: https://github.com/omab/python-social-auth/blob/master/social/apps/pyramid_app/models.py -.. _Webpy App: https://github.com/omab/python-social-auth/blob/master/social/apps/webpy_app/models.py -.. _pipeline docs: pipeline.html#email-validation -.. _transaction: https://pypi.python.org/pypi/transaction diff --git a/docs/strategies.rst b/docs/strategies.rst deleted file mode 100644 index 72e014e5c..000000000 --- a/docs/strategies.rst +++ /dev/null @@ -1,77 +0,0 @@ -Strategies -========== - -Different strategies are defined to encapsulate the different frameworks -capabilities under a common API to reuse as much code as possible. - - -Description ------------ - -A strategy's responsibility is to provide access to: - - * Request data and host information and URI building - * Session access - * Project settings - * Response types (HTML and redirects) - * HTML rendering - -Different frameworks implement these features on different ways, thus the need -for these interfaces. - - -Implementing a new Strategy ---------------------------- - -The following methods must be defined on strategies sub-classes. - -Request:: - - def request_data(self): - """Return current request data (POST or GET)""" - raise NotImplementedError('Implement in subclass') - - def request_host(self): - """Return current host value""" - raise NotImplementedError('Implement in subclass') - - def build_absolute_uri(self, path=None): - """Build absolute URI with given (optional) path""" - raise NotImplementedError('Implement in subclass') - - -Session:: - - def session_get(self, name): - """Return session value for given key""" - raise NotImplementedError('Implement in subclass') - - def session_set(self, name, value): - """Set session value for given key""" - raise NotImplementedError('Implement in subclass') - - def session_pop(self, name): - """Pop session value for given key""" - raise NotImplementedError('Implement in subclass') - - -Settings:: - - def get_setting(self, name): - """Return value for given setting name""" - raise NotImplementedError('Implement in subclass') - - -Responses:: - - def html(self, content): - """Return HTTP response with given content""" - raise NotImplementedError('Implement in subclass') - - def redirect(self, url): - """Return a response redirect to the given URL""" - raise NotImplementedError('Implement in subclass') - - def render_html(self, tpl=None, html=None, context=None): - """Render given template or raw html with given context""" - raise NotImplementedError('Implement in subclass') diff --git a/docs/tests.rst b/docs/tests.rst deleted file mode 100644 index 656f483af..000000000 --- a/docs/tests.rst +++ /dev/null @@ -1,48 +0,0 @@ -Testing python-social-auth -========================== - -Testing the application is fair simple, just met the dependencies and run the -testing suite. - -The testing suite uses HTTPretty_ to mock server responses, it's not a live -test against the providers API, to do it that way, a browser and a tool like -Selenium are needed, that's slow, prone to errors on some cases, and some of -the application examples must be running to perform the testing. Plus real Key -and Secret pairs, in the end it's a mess to test functionality which is the -real point. - -By mocking the server responses, we can test the backends functionality (and -other areas too) easily and quick. - - -Installing dependencies ------------------------ - -Go to the tests_ directory and install the dependencies listed in the -requirements.txt_. Then run with ``nosetests`` command, or with the -``run_tests.sh`` script. - -Tox ---- - -You can use tox_ to test compatibility against all supported Python versions: - -.. code-block:: bash - - $ pip install tox # if not present - $ tox - - -Pending -------- - -At the moment only OAuth1, OAuth2 and OpenId backends are being tested, and -just login and partial pipeline features are covered by the test. There's still -a lot to work on, like: - - * Frameworks support - -.. _HTTPretty: https://github.com/gabrielfalcao/HTTPretty -.. _tests: https://github.com/omab/python-social-auth/tree/master/tests -.. _requirements.txt: https://github.com/omab/python-social-auth/blob/master/tests/requirements.txt -.. _tox: http://tox.readthedocs.org/ diff --git a/docs/thanks.rst b/docs/thanks.rst deleted file mode 100644 index 46d7a2078..000000000 --- a/docs/thanks.rst +++ /dev/null @@ -1,219 +0,0 @@ -Thanks -====== - - -python-social-auth_ is the result of almost 3 years of development done on -django-social-auth_ which is the result of my initial work and the thousands -lines of code contributed by so many developers that took time to work on -improvements, report bugs and hunt them down to propose a fix. So, here is -a big list of users that helped to build this library (if somebody is missed -let me know and I'll update the list): - - * kjoconnor_ - * krvss_ - * estebistec_ - * mrmch_ - * uruz_ - * maraujop_ - * bacher09_ - * dokterbob_ - * hassek_ - * andrusha_ - * vicalloy_ - * caioariede_ - * danielgtaylor_ - * stephenmcd_ - * gugu_ - * yrik_ - * dhendo_ - * yekibud_ - * tmackenzie_ - * LuanP_ - * jezdez_ - * serdardalgic_ - * Jolmberg_ - * ChrisCooper_ - * marselester_ - * eshellman_ - * micrypt_ - * revolunet_ - * dasevilla_ - * seansay_ - * hepochen_ - * gibuloto_ - * crodjer_ - * sidmitra_ - * ryr_ - * inve1_ - * mback2k_ - * hannesstruss_ - * NorthIsUp_ - * tonyxiao_ - * dhepper_ - * Troytft_ - * gardaud_ - * oinopion_ - * gameguy43_ - * vinigracindo_ - * syabro_ - * bashmish_ - * ggreer_ - * avillavi_ - * r4vi_ - * roderyc_ - * daonb_ - * slon7_ - * JasonGiedymin_ - * tymofij_ - * Cassus_ - * martey_ - * t0m_ - * johnthedebs_ - * jammons_ - * stefanw_ - * maxgrosse_ - * mattucf_ - * tadeo_ - * haxoza_ - * bradbeattie_ - * henward0_ - * bernardokyotoku_ - * czpython_ - * glasscube42_ - * assiotis_ - * dbaxa_ - * JasonSanford_ - * originell_ - * cihann_ - * niftynei_ - * mikesun_ - * 1st_ - * betonimig_ - * ozexpert_ - * stephenLee_ - * julianvargasalvarez_ - * youngrok_ - * garrypolley_ - * amirouche_ - * fmoga_ - * pydanny_ - * pygeek_ - * dgouldin_ - * kotslon_ - * kirkchris_ - * barracel_ - * sayar_ - * kulbir_ - * Morgul_ - * spstpl_ - * bluszcz_ - * vbsteven_ - * sbassi_ - * aspcanada_ - * browniebroke_ - - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _django-social-auth: https://github.com/omab/django-social-auth -.. _kjoconnor: https://github.com/kjoconnor -.. _krvss: https://github.com/krvss -.. _estebistec: https://github.com/estebistec -.. _mrmch: https://github.com/mrmch -.. _uruz: https://github.com/uruz -.. _maraujop: https://github.com/maraujop -.. _bacher09: https://github.com/bacher09 -.. _dokterbob: https://github.com/dokterbob -.. _hassek: https://github.com/hassek -.. _andrusha: https://github.com/andrusha -.. _vicalloy: https://github.com/vicalloy -.. _caioariede: https://github.com/caioariede -.. _danielgtaylor: https://github.com/danielgtaylor -.. _stephenmcd: https://github.com/stephenmcd -.. _gugu: https://github.com/gugu -.. _yrik: https://github.com/yrik -.. _dhendo: https://github.com/dhendo -.. _yekibud: https://github.com/yekibud -.. _tmackenzie: https://github.com/tmackenzie -.. _LuanP: https://github.com/LuanP -.. _jezdez: https://github.com/jezdez -.. _serdardalgic: https://github.com/serdardalgic -.. _Jolmberg: https://github.com/Jolmberg -.. _ChrisCooper: https://github.com/ChrisCooper -.. _marselester: https://github.com/marselester -.. _eshellman: https://github.com/eshellman -.. _micrypt: https://github.com/micrypt -.. _revolunet: https://github.com/revolunet -.. _dasevilla: https://github.com/dasevilla -.. _seansay: https://github.com/seansay -.. _hepochen: https://github.com/hepochen -.. _gibuloto: https://github.com/gibuloto -.. _crodjer: https://github.com/crodjer -.. _sidmitra: https://github.com/sidmitra -.. _ryr: https://github.com/ryr -.. _inve1: https://github.com/inve1 -.. _mback2k: https://github.com/mback2k -.. _hannesstruss: https://github.com/hannesstruss -.. _NorthIsUp: https://github.com/NorthIsUp -.. _tonyxiao: https://github.com/tonyxiao -.. _dhepper: https://github.com/dhepper -.. _Troytft: https://github.com/Troytft -.. _gardaud: https://github.com/gardaud -.. _oinopion: https://github.com/oinopion -.. _gameguy43: https://github.com/gameguy43 -.. _vinigracindo: https://github.com/vinigracindo -.. _syabro: https://github.com/syabro -.. _bashmish: https://github.com/bashmish -.. _ggreer: https://github.com/ggreer -.. _avillavi: https://github.com/avillavi -.. _r4vi: https://github.com/r4vi -.. _roderyc: https://github.com/roderyc -.. _daonb: https://github.com/daonb -.. _slon7: https://github.com/slon7 -.. _JasonGiedymin: https://github.com/JasonGiedymin -.. _tymofij: https://github.com/tymofij -.. _Cassus: https://github.com/Cassus -.. _martey: https://github.com/martey -.. _t0m: https://github.com/t0m -.. _johnthedebs: https://github.com/johnthedebs -.. _jammons: https://github.com/jammons -.. _stefanw: https://github.com/stefanw -.. _maxgrosse: https://github.com/maxgrosse -.. _mattucf: https://github.com/mattucf -.. _tadeo: https://github.com/tadeo -.. _haxoza: https://github.com/haxoza -.. _bradbeattie: https://github.com/bradbeattie -.. _henward0: https://github.com/henward0 -.. _bernardokyotoku: https://github.com/bernardokyotoku -.. _czpython: https://github.com/czpython -.. _glasscube42: https://github.com/glasscube42 -.. _assiotis: https://github.com/assiotis -.. _dbaxa: https://github.com/dbaxa -.. _JasonSanford: https://github.com/JasonSanford -.. _originell: https://github.com/originell -.. _cihann: https://github.com/cihann -.. _niftynei: https://github.com/niftynei -.. _mikesun: https://github.com/mikesun -.. _1st: https://github.com/1st -.. _betonimig: https://github.com/betonimig -.. _ozexpert: https://github.com/ozexpert -.. _stephenLee: https://github.com/stephenLee -.. _julianvargasalvarez: https://github.com/julianvargasalvarez -.. _youngrok: https://github.com/youngrok -.. _garrypolley: https://github.com/garrypolley -.. _amirouche: https://github.com/amirouche -.. _fmoga: https://github.com/fmoga -.. _pydanny: https://github.com/pydanny -.. _pygeek: https://github.com/pygeek -.. _dgouldin: https://github.com/dgouldin -.. _kotslon: https://github.com/kotslon -.. _kirkchris: https://github.com/kirkchris -.. _barracel: https://github.com/barracel -.. _sayar: https://github.com/sayar -.. _kulbir: https://github.com/kulbir -.. _Morgul: https://github.com/Morgul -.. _spstpl: https://github.com/spstpl -.. _bluszcz: https://github.com/bluszcz -.. _vbsteven: https://github.com/vbsteven -.. _sbassi: https://github.com/sbassi -.. _aspcanada: https://github.com/aspcanada -.. _browniebroke: https://github.com/browniebroke diff --git a/docs/use_cases.rst b/docs/use_cases.rst deleted file mode 100644 index 4f7c92641..000000000 --- a/docs/use_cases.rst +++ /dev/null @@ -1,312 +0,0 @@ -Use Cases -========= - -Some miscellaneous options and use cases for python-social-auth_. - - -Return the user to the original page ------------------------------------- - -There's a common scenario to return the user back to the original page from -where they requested to login. For that purpose, the usual ``next`` query-string -argument is used. The value of this parameter will be stored in the session and -later used to redirect the user when login was successful. - -In order to use it, just define it with your link. For instance, when using -Django:: - - Login with Facebook - - -Pass custom GET/POST parameters and retrieve them on authentication -------------------------------------------------------------------- - -In some cases, you might need to send data over the URL, and retrieve it while -processing the after-effect. For example, for conditionally executing code in -custom pipelines. - -In such cases, add it to ``FIELDS_STORED_IN_SESSION``. - -In your settings:: - - FIELDS_STORED_IN_SESSION = ['key'] - -In template:: - - Login with Facebook - -In your custom pipeline, retrieve it using:: - - strategy.session_get('key') - - - -Retrieve Google+ Friends ------------------------- - -Google provides a `People API endpoint`_ to retrieve the people in your circles -on Google+. In order to access that API first we need to define the needed -scope:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ - 'https://www.googleapis.com/auth/plus.login' - ] - -Once we have the ``access token`` we can call the API like this:: - - import requests - - user = User.objects.get(...) - social = user.social_auth.get(provider='google-oauth2') - response = requests.get( - 'https://www.googleapis.com/plus/v1/people/me/people/visible', - params={'access_token': social.extra_data['access_token']} - ) - friends = response.json()['items'] - - -Associate users by email ------------------------- - -Sometimes it's desirable that social accounts are automatically associated if -the email already matches a user account. - -For example, if a user signed up with his Facebook account, then logged out and -next time tries to use Google OAuth2 to login, it could be nice (if both social -sites have the same email address configured) that the user gets into his -initial account created by Facebook backend. - -This scenario is possible by enabling the ``associate_by_email`` pipeline -function, like this:: - - SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', - 'social.pipeline.social_auth.associate_by_email', # <--- enable this one - 'social.pipeline.user.create_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - ) - -This feature is disabled by default because it's not 100% secure to automate -this process with all the backends. Not all the providers will validate your -email account and others users could take advantage of that. - -Take for instance User A registered in your site with the email -``foo@bar.com``. Then a malicious user registers into another provider that -doesn't validate his email with that same account. Finally this user will turn -to your site (which supports that provider) and sign up to it, since the email -is the same, the malicious user will take control over the User A account. - - -Signup by OAuth access_token ----------------------------- - -It's a common scenario that mobile applications will use an SDK to signup -a user within the app, but that signup won't be reflected by -python-social-auth_ unless the corresponding database entries are created. In -order to do so, it's possible to create a view / route that creates those -entries by a given ``access_token``. Take the following code for instance (the -code follows Django conventions, but versions for others frameworks can be -implemented easily):: - - from django.contrib.auth import login - - from social.apps.django_app.utils import psa - - # Define an URL entry to point to this view, call it passing the - # access_token parameter like ?access_token=. The URL entry must - # contain the backend, like this: - # - # url(r'^register-by-token/(?P[^/]+)/$', - # 'register_by_access_token') - - @psa('social:complete') - def register_by_access_token(request, backend): - # This view expects an access_token GET parameter, if it's needed, - # request.backend and request.strategy will be loaded with the current - # backend and strategy. - token = request.GET.get('access_token') - user = request.backend.do_auth(request.GET.get('access_token')) - if user: - login(request, user) - return 'OK' - else: - return 'ERROR' - -The snippet above is quite simple, it doesn't return JSON and usually this call -will be done by AJAX. It doesn't return the user information, but that's -something that can be extended and filled to suit the project where it's going -to be used. - - -Multiple scopes per provider ----------------------------- - -At the moment python-social-auth_ doesn't provide a method to define multiple -scopes for single backend, this is usually desired since it's recommended to -ask the user for the minimum scope possible and increase the access when it's -really needed. It's possible to add a new backend extending the original one to -accomplish that behavior. There are two ways to do it. - -1. Overriding ``get_scope()`` method:: - - from social.backends.facebook import FacebookOAuth2 - - - class CustomFacebookOAuth2(FacebookOauth2): - def get_scope(self): - scope = super(CustomFacebookOAuth2, self).get_scope() - if self.data.get('extrascope'): - scope = scope + [('foo', 'bar')] - return scope - - - This method is quite simple, it overrides the method that returns the scope - value in a backend (``get_scope()``) and adds extra values tot he list if it - was indicated by a parameter in the ``GET`` or ``POST`` data - (``self.data``). - - Put this new backend in some place in your project and replace the original - ``FacebookOAuth2`` in ``AUTHENTICATION_BACKENDS`` with this new version. - - When overriding this method, take into account that the default output the - base class for ``get_scope()`` is the raw value from the settings (whatever - they are defined), doing this will actually update the value in your - settings for all the users:: - - scope = super(CustomFacebookOAuth2, self).get_scope() - scope += ['foo', 'bar'] - - Instead do it like this:: - - scope = super(CustomFacebookOAuth2, self).get_scope() - scope = scope + ['foo', 'bar'] - -2. It's possible to do the same by defining a second backend which extends from - the original but overrides the name, this will imply new URLs and also new - settings for the new backend (since the name is used to build the settings - names), it also implies a new application in the provider since not all - providers give you the option of defining multiple redirect URLs. To do it - just add a backend like:: - - from social.backends.facebook import FacebookOAuth2 - - - class CustomFacebookOAuth2(FacebookOauth2): - name = 'facebook-custom' - - Put this new backend in some place in your project keeping the original - ``FacebookOAuth2`` in ``AUTHENTICATION_BACKENDS``. Now a new set of URLs - will be functional:: - - /login/facebook-custom - /complete/facebook-custom - /disconnect/facebook-custom - - And also a new set of settings:: - - SOCIAL_AUTH_FACEBOOK_CUSTOM_KEY = '...' - SOCIAL_AUTH_FACEBOOK_CUSTOM_SECRET = '...' - SOCIAL_AUTH_FACEBOOK_CUSTOM_SCOPE = [...] - - When the extra permissions are needed, just redirect the user to - ``/login/facebook-custom`` and then get the social auth entry for this new - backend with ``user.social_auth.get(provider='facebook-custom')`` and use - the ``access_token`` in it. - - -Enable a user to choose a username from his World of Warcraft characters ------------------------------------------------------------------------- - -If you want to register new users on your site via battle.net, you can enable -these users to choose a username from their own World-of-Warcraft characters. -To do this, use the ``battlenet-oauth2`` backend along with a small form to -choose the username. - -The form is rendered via a partial pipeline item like this:: - - @partial - def pick_character_name(backend, details, response, is_new=False, *args, **kwargs): - if backend.name == 'battlenet-oauth2' and is_new: - data = backend.strategy.request_data() - if data.get('character_name') is None: - # New user and didn't pick a character name yet, so we render - # and send a form to pick one. The form must do a POST/GET - # request to the same URL (/complete/battlenet-oauth2/). In this - # example we expect the user option under the key: - # character_name - # you have to filter the result list according to your needs. - # In this example, only guild members are allowed to sign up. - char_list = [ - c['name'] for c in backend.get_characters(response.get('access_token')) - if 'guild' in c and c['guild'] == '' - ] - return render_to_response('pick_character_form.html', {'charlist': char_list, }) - else: - # The user selected a character name - return {'username': data.get('character_name')} - -Don't forget to add the partial to the pipeline:: - - SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', - 'path.to.pick_character_name', - 'social.pipeline.user.create_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - ) - -It needs to be somewhere before create_user because the partial will change the -username according to the users choice. - - -Re-prompt Google OAuth2 users to refresh the ``refresh_token`` --------------------------------------------------------------- - -A ``refresh_token`` also expire, a ``refresh_token`` can be lost, but they can -also be refreshed (or re-fetched) if you ask to Google the right way. In order -to do so, set this setting:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = { - 'access_type': 'offline', - 'approval_prompt': 'auto' - } - -Then link the users to ``/login/google-oauth2?approval_prompt=force``. If you -want to refresh the ``refresh_token`` only on those users that don't have it, -do it with a pipeline function:: - - def redirect_if_no_refresh_token(backend, response, social, *args, **kwargs): - if backend.name == 'google-oauth2' and social and \ - response.get('refresh_token') is None and \ - social.extra_data.get('refresh_token') is None: - return redirect('/login/google-oauth2?approval_prompt=force') - -Set this pipeline after ``social_user``:: - - SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'path.to.redirect_if_no_refresh_token', - 'social.pipeline.user.get_username', - 'social.pipeline.user.create_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - ) - - -.. _python-social-auth: https://github.com/omab/python-social-auth -.. _People API endpoint: https://developers.google.com/+/api/latest/people/list diff --git a/requirements-python3.txt b/requirements-python3.txt index a126ab21b..4e8d6b68a 100644 --- a/requirements-python3.txt +++ b/requirements-python3.txt @@ -1,20 +1 @@ -python3-openid>=3.0.9 -requests>=2.9.1 -oauthlib>=1.0.3 -requests-oauthlib>=0.6.1 -six>=1.10.0 -PyJWT>=1.4.0 social-auth-core -social-auth-storage-mongoengine -social-auth-storage-peewee -social-auth-storage-sqlalchemy -social-auth-app-cherrypy -social-auth-app-django -social-auth-app-django-mongoengine -social-auth-app-flask -social-auth-app-flask-mongoengine -social-auth-app-flask-peewee -social-auth-app-flask-sqlalchemy -social-auth-app-pyramid -social-auth-app-tornado -social-auth-app-webpy diff --git a/site/css/bootstrap-responsive.min.css b/site/css/bootstrap-responsive.min.css deleted file mode 100644 index 059786010..000000000 --- a/site/css/bootstrap-responsive.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap Responsive v2.3.0 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/site/css/bootstrap.min.css b/site/css/bootstrap.min.css deleted file mode 100644 index fd5ed7340..000000000 --- a/site/css/bootstrap.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap v2.3.0 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/site/css/site.css b/site/css/site.css deleted file mode 100644 index 0292b3b71..000000000 --- a/site/css/site.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - padding-top: 60px; - padding-bottom: 40px; -} diff --git a/site/docs b/site/docs deleted file mode 120000 index 16bfa418b..000000000 --- a/site/docs +++ /dev/null @@ -1 +0,0 @@ -../docs/_build/ \ No newline at end of file diff --git a/site/img/glyphicons-halflings-white.png b/site/img/glyphicons-halflings-white.png deleted file mode 100644 index 3bf6484a29d8da269f9bc874b25493a45fae3bae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8777 zcmZvC1yGz#v+m*$LXcp=A$ZWB0fL7wNbp_U*$~{_gL`my3oP#L!5tQYy99Ta`+g_q zKlj|KJ2f@c)ARJx{q*bbkhN_!|Wn*Vos8{TEhUT@5e;_WJsIMMcG5%>DiS&dv_N`4@J0cnAQ-#>RjZ z00W5t&tJ^l-QC*ST1-p~00u^9XJ=AUl7oW-;2a+x2k__T=grN{+1c4XK0ZL~^z^i$ zp&>vEhr@4fZWb380S18T&!0cQ3IKpHF)?v=b_NIm0Q>vwY7D0baZ)n z31Fa5sELUQARIVaU0nqf0XzT+fB_63aA;@<$l~wse|mcA;^G1TmX?-)e)jkGPfkuA z92@|!<>h5S_4f8QP-JRq>d&7)^Yin8l7K8gED$&_FaV?gY+wLjpoW%~7NDe=nHfMG z5DO3j{R9kv5GbssrUpO)OyvVrlx>u0UKD0i;Dpm5S5dY16(DL5l{ixz|mhJU@&-OWCTb7_%}8-fE(P~+XIRO zJU|wp1|S>|J3KrLcz^+v1f&BDpd>&MAaibR4#5A_4(MucZwG9E1h4@u0P@C8;oo+g zIVj7kfJi{oV~E(NZ*h(@^-(Q(C`Psb3KZ{N;^GB(a8NE*Vwc715!9 zr-H4Ao|T_c6+VT_JH9H+P3>iXSt!a$F`>s`jn`w9GZ_~B!{0soaiV|O_c^R2aWa%}O3jUE)WO=pa zs~_Wz08z|ieY5A%$@FcBF9^!1a}m5ks@7gjn;67N>}S~Hrm`4sM5Hh`q7&5-N{|31 z6x1{ol7BnskoViZ0GqbLa#kW`Z)VCjt1MysKg|rT zi!?s##Ck>8c zpi|>$lGlw#@yMNi&V4`6OBGJ(H&7lqLlcTQ&1zWriG_fL>BnFcr~?;E93{M-xIozQ zO=EHQ#+?<}%@wbWWv23#!V70h9MOuUVaU>3kpTvYfc|LBw?&b*89~Gc9i&8tlT#kF ztpbZoAzkdB+UTy=tx%L3Z4)I{zY(Kb)eg{InobSJmNwPZt$14aS-uc4eKuY8h$dtfyxu^a%zA)>fYI&)@ZXky?^{5>xSC?;w4r&td6vBdi%vHm4=XJH!3yL3?Ep+T5aU_>i;yr_XGq zxZfCzUU@GvnoIk+_Nd`aky>S&H!b*{A%L>?*XPAgWL(Vf(k7qUS}>Zn=U(ZfcOc{B z3*tOHH@t5Ub5D~#N7!Fxx}P2)sy{vE_l(R7$aW&CX>c|&HY+7};vUIietK%}!phrCuh+;C@1usp;XLU<8Gq8P!rEI3ieg#W$!= zQcZr{hp>8sF?k&Yl0?B84OneiQxef-4TEFrq3O~JAZR}yEJHA|Xkqd49tR&8oq{zP zY@>J^HBV*(gJvJZc_0VFN7Sx?H7#75E3#?N8Z!C+_f53YU}pyggxx1?wQi5Yb-_`I`_V*SMx5+*P^b=ec5RON-k1cIlsBLk}(HiaJyab0`CI zo0{=1_LO$~oE2%Tl_}KURuX<`+mQN_sTdM&* zkFf!Xtl^e^gTy6ON=&gTn6)$JHQq2)33R@_!#9?BLNq-Wi{U|rVX7Vny$l6#+SZ@KvQt@VYb%<9JfapI^b9j=wa+Tqb4ei;8c5 z&1>Uz@lVFv6T4Z*YU$r4G`g=91lSeA<=GRZ!*KTWKDPR}NPUW%peCUj`Ix_LDq!8| zMH-V`Pv!a~QkTL||L@cqiTz)*G-0=ytr1KqTuFPan9y4gYD5>PleK`NZB$ev@W%t= zkp)_=lBUTLZJpAtZg;pjI;7r2y|26-N7&a(hX|`1YNM9N8{>8JAuv}hp1v`3JHT-=5lbXpbMq7X~2J5Kl zh7tyU`_AusMFZ{ej9D;Uyy;SQ!4nwgSnngsYBwdS&EO3NS*o04)*juAYl;57c2Ly0(DEZ8IY?zSph-kyxu+D`tt@oU{32J#I{vmy=#0ySPK zA+i(A3yl)qmTz*$dZi#y9FS;$;h%bY+;StNx{_R56Otq+?pGe^T^{5d7Gs&?`_r`8 zD&dzOA|j8@3A&FR5U3*eQNBf<4^4W_iS_()*8b4aaUzfk2 zzIcMWSEjm;EPZPk{j{1>oXd}pXAj!NaRm8{Sjz!D=~q3WJ@vmt6ND_?HI~|wUS1j5 z9!S1MKr7%nxoJ3k`GB^7yV~*{n~O~n6($~x5Bu{7s|JyXbAyKI4+tO(zZYMslK;Zc zzeHGVl{`iP@jfSKq>R;{+djJ9n%$%EL()Uw+sykjNQdflkJZSjqV_QDWivbZS~S{K zkE@T^Jcv)Dfm93!mf$XYnCT--_A$zo9MOkPB6&diM8MwOfV?+ApNv`moV@nqn>&lv zYbN1-M|jc~sG|yLN^1R2=`+1ih3jCshg`iP&mY$GMTcY^W^T`WOCX!{-KHmZ#GiRH zYl{|+KLn5!PCLtBy~9i}`#d^gCDDx$+GQb~uc;V#K3OgbbOG0j5{BRG-si%Bo{@lB zGIt+Ain8^C`!*S0d0OSWVO+Z89}}O8aFTZ>p&k}2gGCV zh#<$gswePFxWGT$4DC^8@84_e*^KT74?7n8!$8cg=sL$OlKr&HMh@Rr5%*Wr!xoOl zo7jItnj-xYgVTX)H1=A2bD(tleEH57#V{xAeW_ezISg5OC zg=k>hOLA^urTH_e6*vSYRqCm$J{xo}-x3@HH;bsHD1Z`Pzvsn}%cvfw%Q(}h`Dgtb z0_J^niUmoCM5$*f)6}}qi(u;cPgxfyeVaaVmOsG<)5`6tzU4wyhF;k|~|x>7-2hXpVBpc5k{L4M`Wbe6Q?tr^*B z`Y*>6*&R#~%JlBIitlZ^qGe3s21~h3U|&k%%jeMM;6!~UH|+0+<5V-_zDqZQN79?n?!Aj!Nj`YMO9?j>uqI9-Tex+nJD z%e0#Yca6(zqGUR|KITa?9x-#C0!JKJHO(+fy@1!B$%ZwJwncQW7vGYv?~!^`#L~Um zOL++>4qmqW`0Chc0T23G8|vO)tK=Z2`gvS4*qpqhIJCEv9i&&$09VO8YOz|oZ+ubd zNXVdLc&p=KsSgtmIPLN69P7xYkYQ1vJ?u1g)T!6Ru`k2wkdj*wDC)VryGu2=yb0?F z>q~~e>KZ0d_#7f3UgV%9MY1}vMgF{B8yfE{HL*pMyhYF)WDZ^^3vS8F zGlOhs%g_~pS3=WQ#494@jAXwOtr^Y|TnQ5zki>qRG)(oPY*f}U_=ip_{qB0!%w7~G zWE!P4p3khyW-JJnE>eECuYfI?^d366Shq!Wm#x&jAo>=HdCllE$>DPO0N;y#4G)D2y#B@5=N=+F%Xo2n{gKcPcK2!hP*^WSXl+ut; zyLvVoY>VL{H%Kd9^i~lsb8j4>$EllrparEOJNT?Ym>vJa$(P^tOG)5aVb_5w^*&M0 zYOJ`I`}9}UoSnYg#E(&yyK(tqr^@n}qU2H2DhkK-`2He% zgXr_4kpXoQHxAO9S`wEdmqGU4j=1JdG!OixdqB4PPP6RXA}>GM zumruUUH|ZG2$bBj)Qluj&uB=dRb)?^qomw?Z$X%#D+Q*O97eHrgVB2*mR$bFBU`*} zIem?dM)i}raTFDn@5^caxE^XFXVhBePmH9fqcTi`TLaXiueH=@06sl}>F%}h9H_e9 z>^O?LxM1EjX}NVppaO@NNQr=AtHcH-BU{yBT_vejJ#J)l^cl69Z7$sk`82Zyw7Wxt z=~J?hZm{f@W}|96FUJfy65Gk8?^{^yjhOahUMCNNpt5DJw}ZKH7b!bGiFY9y6OY&T z_N)?Jj(MuLTN36ZCJ6I5Xy7uVlrb$o*Z%=-)kPo9s?<^Yqz~!Z* z_mP8(unFq65XSi!$@YtieSQ!<7IEOaA9VkKI?lA`*(nURvfKL8cX}-+~uw9|_5)uC2`ZHcaeX7L8aG6Ghleg@F9aG%X$#g6^yP5apnB>YTz&EfS{q z9UVfSyEIczebC)qlVu5cOoMzS_jrC|)rQlAzK7sfiW0`M8mVIohazPE9Jzn*qPt%6 zZL8RELY@L09B83@Be;x5V-IHnn$}{RAT#<2JA%ttlk#^(%u}CGze|1JY5MPhbfnYG zIw%$XfBmA-<_pKLpGKwbRF$#P;@_)ech#>vj25sv25VM$ouo)?BXdRcO{)*OwTw)G zv43W~T6ekBMtUD%5Bm>`^Ltv!w4~65N!Ut5twl!Agrzyq4O2Fi3pUMtCU~>9gt_=h-f% z;1&OuSu?A_sJvIvQ+dZNo3?m1%b1+s&UAx?8sUHEe_sB7zkm4R%6)<@oYB_i5>3Ip zIA+?jVdX|zL{)?TGpx+=Ta>G80}0}Ax+722$XFNJsC1gcH56{8B)*)eU#r~HrC&}` z|EWW92&;6y;3}!L5zXa385@?-D%>dSvyK;?jqU2t_R3wvBW;$!j45uQ7tyEIQva;Db}r&bR3kqNSh)Q_$MJ#Uj3Gj1F;)sO|%6z#@<+ zi{pbYsYS#u`X$Nf($OS+lhw>xgjos1OnF^$-I$u;qhJswhH~p|ab*nO>zBrtb0ndn zxV0uh!LN`&xckTP+JW}gznSpU492)u+`f{9Yr)js`NmfYH#Wdtradc0TnKNz@Su!e zu$9}G_=ku;%4xk}eXl>)KgpuT>_<`Ud(A^a++K&pm3LbN;gI}ku@YVrA%FJBZ5$;m zobR8}OLtW4-i+qPPLS-(7<>M{)rhiPoi@?&vDeVq5%fmZk=mDdRV>Pb-l7pP1y6|J z8I>sF+TypKV=_^NwBU^>4JJq<*14GLfM2*XQzYdlqqjnE)gZsPW^E@mp&ww* zW9i>XL=uwLVZ9pO*8K>t>vdL~Ek_NUL$?LQi5sc#1Q-f6-ywKcIT8Kw?C(_3pbR`e|)%9S-({if|E+hR2W!&qfQ&UiF^I!|M#xhdWsenv^wpKCBiuxXbnp85`{i|;BM?Ba`lqTA zyRm=UWJl&E{8JzYDHFu>*Z10-?#A8D|5jW9Ho0*CAs0fAy~MqbwYuOq9jjt9*nuHI zbDwKvh)5Ir$r!fS5|;?Dt>V+@F*v8=TJJF)TdnC#Mk>+tGDGCw;A~^PC`gUt*<(|i zB{{g{`uFehu`$fm4)&k7`u{xIV)yvA(%5SxX9MS80p2EKnLtCZ>tlX>*Z6nd&6-Mv$5rHD*db;&IBK3KH&M<+ArlGXDRdX1VVO4)&R$f4NxXI>GBh zSv|h>5GDAI(4E`@F?EnW zS>#c&Gw6~_XL`qQG4bK`W*>hek4LX*efn6|_MY+rXkNyAuu?NxS%L7~9tD3cn7&p( zCtfqe6sjB&Q-Vs7BP5+%;#Gk};4xtwU!KY0XXbmkUy$kR9)!~?*v)qw00!+Yg^#H> zc#8*z6zZo>+(bud?K<*!QO4ehiTCK&PD4G&n)Tr9X_3r-we z?fI+}-G~Yn93gI6F{}Dw_SC*FLZ)5(85zp4%uubtD)J)UELLkvGk4#tw&Tussa)mTD$R2&O~{ zCI3>fr-!-b@EGRI%g0L8UU%%u_<;e9439JNV;4KSxd|78v+I+8^rmMf3f40Jb}wEszROD?xBZu>Ll3;sUIoNxDK3|j3*sam2tC@@e$ z^!;+AK>efeBJB%ALsQ{uFui)oDoq()2USi?n=6C3#eetz?wPswc={I<8x=(8lE4EIsUfyGNZ{|KYn1IR|=E==f z(;!A5(-2y^2xRFCSPqzHAZn5RCN_bp22T(KEtjA(rFZ%>a4@STrHZflxKoqe9Z4@^ zM*scx_y73?Q{vt6?~WEl?2q*;@8 z3M*&@%l)SQmXkcUm)d@GT2#JdzhfSAP9|n#C;$E8X|pwD!r#X?0P>0ZisQ~TNqupW z*lUY~+ikD`vQb?@SAWX#r*Y+;=_|oacL$2CL$^(mV}aKO77pg}O+-=T1oLBT5sL2i z42Qth2+0@C`c+*D0*5!qy26sis<9a7>LN2{z%Qj49t z=L@x`4$ALHb*3COHoT?5S_c(Hs}g!V>W^=6Q0}zaubkDn)(lTax0+!+%B}9Vqw6{H zvL|BRM`O<@;eVi1DzM!tXtBrA20Ce@^Jz|>%X-t`vi-%WweXCh_LhI#bUg2*pcP~R z*RuTUzBKLXO~~uMd&o$v3@d0shHfUjC6c539PE6rF&;Ufa(Rw@K1*m7?f5)t`MjH0 z)_V(cajV5Am>f!kWcI@5rE8t6$S>5M=k=aRZROH6fA^jJp~2NlR4;Q2>L$7F#RT#9 z>4@1RhWG`Khy>P2j1Yx^BBL{S`niMaxlSWV-JBU0-T9zZ%>7mR3l$~QV$({o0;jTI ze5=cN^!Bc2bT|BcojXp~K#2cM>OTe*cM{Kg-j*CkiW)EGQot^}s;cy8_1_@JA0Whq zlrNr+R;Efa+`6N)s5rH*|E)nYZ3uqkk2C(E7@A|3YI`ozP~9Lexx#*1(r8luq+YPk z{J}c$s` zPM35Fx(YWB3Z5IYnN+L_4|jaR(5iWJi2~l&xy}aU7kW?o-V*6Av2wyZTG!E2KSW2* zGRLQkQU;Oz##ie-Z4fI)WSRxn$(ZcD;TL+;^r=a4(G~H3ZhK$lSXZj?cvyY8%d9JM zzc3#pD^W_QnWy#rx#;c&N@sqHhrnHRmj#i;s%zLm6SE(n&BWpd&f7>XnjV}OlZntI70fq%8~9<7 zMYaw`E-rp49-oC1N_uZTo)Cu%RR2QWdHpzQIcNsoDp`3xfP+`gI?tVQZ4X={qU?(n zV>0ASES^Xuc;9JBji{)RnFL(Lez;8XbB1uWaMp@p?7xhXk6V#!6B@aP4Rz7-K%a>i z?fvf}va_DGUXlI#4--`A3qK7J?-HwnG7O~H2;zR~RLW)_^#La!=}+>KW#anZ{|^D3 B7G?kd diff --git a/site/img/glyphicons-halflings.png b/site/img/glyphicons-halflings.png deleted file mode 100644 index a9969993201f9cee63cf9f49217646347297b643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12799 zcma*OWmH^Ivn@*S;K3nSf_t!#;0f+&pm7Po8`nk}2q8f5;M%x$SdAkd9FAvlc$ zx660V9e3Ox@4WZ^?7jZ%QFGU-T~%||Ug4iK6bbQY@zBuF2$hxOw9wF=A)nUSxR_5@ zEX>HBryGrjyuOFFv$Y4<+|3H@gQfEqD<)+}a~mryD|1U9*I_FOG&F%+Ww{SJ-V2BR zjt<81Ek$}Yb*95D4RS0HCps|uLyovt;P05hchQb-u2bzLtmog&f2}1VlNhxXV);S9 zM2buBg~!q9PtF)&KGRgf3#z7B(hm5WlNClaCWFs!-P!4-u*u5+=+D|ZE9e`KvhTHT zJBnLwGM%!u&vlE%1ytJ=!xt~y_YkFLQb6bS!E+s8l7PiPGSt9xrmg?LV&&SL?J~cI zS(e9TF1?SGyh+M_p@o1dyWu7o7_6p;N6hO!;4~ z2B`I;y`;$ZdtBpvK5%oQ^p4eR2L)BH>B$FQeC*t)c`L71gXHPUa|vyu`Bnz)H$ZcXGve(}XvR!+*8a>BLV;+ryG1kt0=)ytl zNJxFUN{V7P?#|Cp85QTa@(*Q3%K-R(Pkv1N8YU*(d(Y}9?PQ(j;NzWoEVWRD-~H$=f>j9~PN^BM2okI(gY-&_&BCV6RP&I$FnSEM3d=0fCxbxA6~l>54-upTrw zYgX@%m>jsSGi`0cQt6b8cX~+02IghVlNblR7eI;0ps}mpWUcxty1yG56C5rh%ep(X z?)#2d?C<4t-KLc*EAn>>M8%HvC1TyBSoPNg(4id~H8JwO#I)Bf;N*y6ai6K9_bA`4 z_g9(-R;qyH&6I$`b42v|0V3Z8IXN*p*8g$gE98+JpXNY+jXxU0zsR^W$#V=KP z3AEFp@OL}WqwOfsV<)A^UTF4&HF1vQecz?LWE@p^Z2){=KEC_3Iopx_eS42>DeiDG zWMXGbYfG~W7C8s@@m<_?#Gqk;!&)_Key@^0xJxrJahv{B&{^!>TV7TEDZlP|$=ZCz zmX=ZWtt4QZKx**)lQQoW8y-XLiOQy#T`2t}p6l*S`68ojyH@UXJ-b~@tN`WpjF z%7%Yzv807gsO!v=!(2uR)16!&U5~VPrPHtGzUU?2w(b1Xchq}(5Ed^G|SD7IG+kvgyVksU) z(0R)SW1V(>&q2nM%Z!C9=;pTg!(8pPSc%H01urXmQI6Gi^dkYCYfu6b4^tW))b^U+ z$2K&iOgN_OU7n#GC2jgiXU{caO5hZt0(>k+c^(r><#m|#J^s?zA6pi;^#*rp&;aqL zRcZi0Q4HhVX3$ybclxo4FFJW*`IV`)Bj_L3rQe?5{wLJh168Ve1jZv+f1D}f0S$N= zm4i|9cEWz&C9~ZI3q*gwWH^<6sBWuphgy@S3Qy?MJiL>gwd|E<2h9-$3;gT9V~S6r z)cAcmE0KXOwDA5eJ02-75d~f?3;n7a9d_xPBJaO;Z)#@s7gk5$Qn(Fc^w@9c5W0zY z59is0?Mt^@Rolcn{4%)Ioat(kxQH6}hIykSA)zht=9F_W*D#<}N(k&&;k;&gKkWIL z0Of*sP=X(Uyu$Pw;?F@?j{}=>{aSHFcii#78FC^6JGrg-)!)MV4AKz>pXnhVgTgx8 z1&5Y=>|8RGA6++FrSy=__k_imx|z-EI@foKi>tK0Hq2LetjUotCgk2QFXaej!BWYL zJc{fv(&qA7UUJ|AXLc5z*_NW#yWzKtl(c8mEW{A>5Hj^gfZ^HC9lQNQ?RowXjmuCj4!!54Us1=hY z0{@-phvC}yls!PmA~_z>Y&n&IW9FQcj}9(OLO-t^NN$c0o}YksCUWt|DV(MJB%%Sr zdf}8!9ylU2TW!=T{?)g-ojAMKc>3pW;KiZ7f0;&g)k}K^#HBhE5ot)%oxq$*$W@b# zg4p<Ou`ME|Kd1WHK@8 zzLD+0(NHWa`B{em3Ye?@aVsEi>y#0XVZfaFuq#;X5C3{*ikRx7UY4FF{ZtNHNO?A_ z#Q?hwRv~D8fPEc%B5E-ZMI&TAmikl||EERumQCRh7p;)>fdZMxvKq;ky0}7IjhJph zW*uuu*(Y6)S;Od--8uR^R#sb$cmFCnPcj9PPCWhPN;n`i1Q#Qn>ii z{WR|0>8F`vf&#E(c2NsoH=I7Cd-FV|%(7a`i}gZw4N~QFFG2WtS^H%@c?%9UZ+kez z;PwGgg_r6V>Kn5n(nZ40P4qMyrCP3bDkJp@hp6&X3>gzC>=f@Hsen<%I~7W+x@}b> z0}Et*vx_50-q@PIV=(3&Tbm}}QRo*FP2@)A#XX-8jYspIhah`9ukPBr)$8>Tmtg&R z?JBoH17?+1@Y@r>anoKPQ}F8o9?vhcG79Cjv^V6ct709VOQwg{c0Q#rBSsSmK3Q;O zBpNihl3S0_IGVE)^`#94#j~$;7+u870yWiV$@={|GrBmuz4b)*bCOPkaN0{6$MvazOEBxFdKZDlbVvv{8_*kJ zfE6C`4&Kkz<5u%dEdStd85-5UHG5IOWbo8i9azgg#zw-(P1AA049hddAB*UdG3Vn0 zX`OgM+EM|<+KhJ<=k?z~WA5waVj?T9eBdfJGebVifBKS1u<$#vl^BvSg)xsnT5Aw_ZY#}v*LXO#htB>f}x3qDdDHoFeb zAq7;0CW;XJ`d&G*9V)@H&739DpfWYzdQt+Kx_E1K#Cg1EMtFa8eQRk_JuUdHD*2;W zR~XFnl!L2A?48O;_iqCVr1oxEXvOIiN_9CUVTZs3C~P+11}ebyTRLACiJuMIG#`xP zKlC|E(S@QvN+%pBc6vPiQS8KgQAUh75C0a2xcPQDD$}*bM&z~g8+=9ltmkT$;c;s z5_=8%i0H^fEAOQbHXf0;?DN5z-5+1 zDxj50yYkz4ox9p$HbZ|H?8ukAbLE^P$@h}L%i6QVcY>)i!w=hkv2zvrduut%!8>6b zcus3bh1w~L804EZ*s96?GB&F7c5?m?|t$-tp2rKMy>F*=4;w*jW}^;8v`st&8)c; z2Ct2{)?S(Z;@_mjAEjb8x=qAQvx=}S6l9?~H?PmP`-xu;ME*B8sm|!h@BX4>u(xg_ zIHmQzp4Tgf*J}Y=8STR5_s)GKcmgV!$JKTg@LO402{{Wrg>#D4-L%vjmtJ4r?p&$F!o-BOf7ej~ z6)BuK^^g1b#(E>$s`t3i13{6-mmSp7{;QkeG5v}GAN&lM2lQT$@(aQCcFP(%UyZbF z#$HLTqGT^@F#A29b0HqiJsRJAlh8kngU`BDI6 zJUE~&!cQ*&f95Ot$#mxU5+*^$qg_DWNdfu+1irglB7yDglzH()2!@#rpu)^3S8weW z_FE$=j^GTY*|5SH95O8o8W9FluYwB=2PwtbW|JG6kcV^dMVmX(wG+Otj;E$%gfu^K z!t~<3??8=()WQSycsBKy24>NjRtuZ>zxJIED;YXaUz$@0z4rl+TW zWxmvM$%4jYIpO>j5k1t1&}1VKM~s!eLsCVQ`TTjn3JRXZD~>GM z$-IT~(Y)flNqDkC%DfbxaV9?QuWCV&-U1yzrV@0jRhE;)ZO0=r-{s@W?HOFbRHDDV zq;eLo+wOW;nI|#mNf(J?RImB9{YSO2Y`9825Lz#u4(nk3)RGv3X8B(A$TsontJ8L! z9JP^eWxtKC?G8^xAZa1HECx*rp35s!^%;&@Jyk)NexVc)@U4$^X1Dag6`WKs|(HhZ#rzO2KEw3xh~-0<;|zcs0L>OcO#YYX{SN8m6`9pp+ zQG@q$I)T?aoe#AoR@%om_#z=c@ych!bj~lV13Qi-xg$i$hXEAB#l=t7QWENGbma4L zbBf*X*4oNYZUd_;1{Ln_ZeAwQv4z?n9$eoxJeI?lU9^!AB2Y~AwOSq67dT9ADZ)s@ zCRYS7W$Zpkdx$3T>7$I%3EI2ik~m!f7&$Djpt6kZqDWZJ-G{*_eXs*B8$1R4+I}Kf zqniwCI64r;>h2Lu{0c(#Atn)%E8&)=0S4BMhq9$`vu|Ct;^ur~gL`bD>J@l)P$q_A zO7b3HGOUG`vgH{}&&AgrFy%K^>? z>wf**coZ2vdSDcNYSm~dZ(vk6&m6bVKmVgrx-X<>{QzA!)2*L+HLTQz$e8UcB&Djq zl)-%s$ZtUN-R!4ZiG=L0#_P=BbUyH+YPmFl_ogkkQ$=s@T1v}rNnZ^eMaqJ|quc+6 z*ygceDOrldsL30w`H;rNu+IjlS+G~p&0SawXCA1+D zC%cZtjUkLNq%FadtHE?O(yQTP486A{1x<{krq#rpauNQaeyhM3*i0%tBpQHQo-u)x z{0{&KS`>}vf2_}b160XZO2$b)cyrHq7ZSeiSbRvaxnKUH{Q`-P(nL&^fcF2){vhN- zbX&WEjP7?b4A%0y6n_=m%l00uZ+}mCYO(!x?j$+O$*TqoD_Q5EoyDJ?w?^UIa491H zE}87(bR`X;@u#3Qy~9wWdWQIg1`cXrk$x9=ccR|RY1~%{fAJ@uq@J3e872x0v$hmv ze_KcL(wM|n0EOp;t{hKoohYyDmYO;!`7^Lx;0k=PWPGZpI>V5qYlzjSL_(%|mud50 z7#{p97s`U|Sn$WYF>-i{i4`kzlrV6a<}=72q2sAT7Zh{>P%*6B;Zl;~0xWymt10Mo zl5{bmR(wJefJpNGK=fSRP|mpCI-)Nf6?Pv==FcFmpSwF1%CTOucV{yqxSyx4Zws3O z8hr5Uyd%ezIO7?PnEO0T%af#KOiXD$e?V&OX-B|ZX-YsgSs%sv-6U+sLPuz{D4bq| zpd&|o5tNCmpT>(uIbRf?8c}d3IpOb3sn6>_dr*26R#ev<_~vi)wleW$PX|5)$_ z+_|=pi(0D(AB_sjQ;sQQSM&AWqzDO1@NHw;C9cPdXRKRI#@nUW)CgFxzQ1nyd!+h& zcjU!U=&u|>@}R(9D$%lu2TlV>@I2-n@fCr5PrZNVyKWR7hm zWjoy^p7v8m#$qN0K#8jT- zq`mSirDZDa1Jxm;Rg3rAPhC)LcI4@-RvKT+@9&KsR3b0_0zuM!Fg7u>oF>3bzOxZPU&$ab$Z9@ zY)f7pKh22I7ZykL{YsdjcqeN++=0a}elQM-4;Q)(`Ep3|VFHqnXOh14`!Bus& z9w%*EWK6AiAM{s$6~SEQS;A>ey$#`7)khZvamem{P?>k)5&7Sl&&NXKk}o!%vd;-! zpo2p-_h^b$DNBO>{h4JdGB=D>fvGIYN8v&XsfxU~VaefL?q} z3ekM?iOKkCzQHkBkhg=hD!@&(L}FcHKoa zbZ7)H1C|lHjwEb@tu=n^OvdHOo7o+W`0-y3KdP#bb~wM=Vr_gyoEq|#B?$&d$tals ziIs-&7isBpvS|CjC|7C&3I0SE?~`a%g~$PI%;au^cUp@ER3?mn-|vyu!$7MV6(uvt z+CcGuM(Ku2&G0tcRCo7#D$Dirfqef2qPOE5I)oCGzmR5G!o#Q~(k~)c=LpIfrhHQk zeAva6MilEifE7rgP1M7AyWmLOXK}i8?=z2;N=no)`IGm#y%aGE>-FN zyXCp0Sln{IsfOBuCdE*#@CQof%jzuU*jkR*Su3?5t}F(#g0BD0Zzu|1MDes8U7f9; z$JBg|mqTXt`muZ8=Z`3wx$uizZG_7>GI7tcfOHW`C2bKxNOR)XAwRkLOaHS4xwlH4 zDpU29#6wLXI;H?0Se`SRa&I_QmI{zo7p%uveBZ0KZKd9H6@U?YGArbfm)D*^5=&Rp z`k{35?Z5GbZnv>z@NmJ%+sx=1WanWg)8r}C_>EGR8mk(NR$pW<-l8OTU^_u3M@gwS z7}GGa1)`z5G|DZirw;FB@VhH7Dq*0qc=|9lLe{w2#`g+_nt>_%o<~9(VZe=zI*SSz4w43-_o>4E4`M@NPKTWZuQJs)?KXbWp1M zimd5F;?AP(LWcaI-^Sl{`~>tmxsQB9Y$Xi*{Zr#py_+I$vx7@NY`S?HFfS!hUiz$a z{>!&e1(16T!Om)m)&k1W#*d#GslD^4!TwiF2WjFBvi=Ms!ADT)ArEW6zfVuIXcXVk z>AHjPADW+mJzY`_Ieq(s?jbk4iD2Rb8*V3t6?I+E06(K8H!!xnDzO%GB;Z$N-{M|B zeT`jo%9)s%op*XZKDd6*)-^lWO{#RaIGFdBH+;XXjI(8RxpBc~azG1H^2v7c^bkFE zZCVPE+E*Q=FSe8Vm&6|^3ki{9~qafiMAf7i4APZg>b%&5>nT@pHH z%O*pOv(77?ZiT{W zBibx}Q12tRc7Py1NcZTp`Q4ey%T_nj@1WKg5Fz_Rjl4wlJQj)rtp8yL3r!Shy zvZvnmh!tH4T6Js-?vI0<-rzzl{mgT*S0d_7^AU_8gBg^03o-J=p(1o6kww2hx|!%T z-jqp}m^G*W?$!R#M%Ef?&2jYxmx+lXWZszpI4d$pUN`(S)|*c^CgdwY>Fa>> zgGBJhwe8y#Xd*q0=@SLEgPF>+Qe4?%E*v{a`||luZ~&dqMBrRfJ{SDMaJ!s_;cSJp zSqZHXIdc@@XteNySUZs^9SG7xK`8=NBNM)fRVOjw)D^)w%L2OPkTQ$Tel-J)GD3=YXy+F4in(ILy*A3m@3o73uv?JC}Q>f zrY&8SWmesiba0|3X-jmlMT3 z*ST|_U@O=i*sM_*48G)dgXqlwoFp5G6qSM3&%_f_*n!PiT>?cNI)fAUkA{qWnqdMi+aNK_yVQ&lx4UZknAc9FIzVk% zo6JmFH~c{_tK!gt4+o2>)zoP{sR}!!vfRjI=13!z5}ijMFQ4a4?QIg-BE4T6!#%?d&L;`j5=a`4is>U;%@Rd~ zXC~H7eGQhhYWhMPWf9znDbYIgwud(6$W3e>$W4$~d%qoJ z+JE`1g$qJ%>b|z*xCKenmpV$0pM=Gl-Y*LT8K+P)2X#;XYEFF4mRbc~jj?DM@(1e`nL=F4Syv)TKIePQUz)bZ?Bi3@G@HO$Aps1DvDGkYF50O$_welu^cL7;vPiMGho74$;4fDqKbE{U zd1h{;LfM#Fb|Z&uH~Rm_J)R~Vy4b;1?tW_A)Iz#S_=F|~pISaVkCnQ0&u%Yz%o#|! zS-TSg87LUfFSs{tTuM3$!06ZzH&MFtG)X-l7>3)V?Txuj2HyG*5u;EY2_5vU0ujA? zHXh5G%6e3y7v?AjhyX79pnRBVr}RmPmtrxoB7lkxEzChX^(vKd+sLh?SBic=Q)5nA zdz7Mw3_iA>;T^_Kl~?1|5t%GZ;ki_+i>Q~Q1EVdKZ)$Sh3LM@ea&D~{2HOG++7*wF zAC6jW4>fa~!Vp5+$Z{<)Qxb|{unMgCv2)@%3j=7)Zc%U<^i|SAF88s!A^+Xs!OASYT%7;Jx?olg_6NFP1475N z#0s<@E~FI}#LNQ{?B1;t+N$2k*`K$Hxb%#8tRQi*Z#No0J}Pl;HWb){l7{A8(pu#@ zfE-OTvEreoz1+p`9sUI%Y{e5L-oTP_^NkgpYhZjp&ykinnW;(fu1;ttpSsgYM8ABX4dHe_HxU+%M(D=~) zYM}XUJ5guZ;=_ZcOsC`_{CiU$zN3$+x&5C`vX-V3`8&RjlBs^rf00MNYZW+jCd~7N z%{jJuUUwY(M`8$`B>K&_48!Li682ZaRknMgQ3~dnlp8C?__!P2z@=Auv;T^$yrsNy zCARmaA@^Yo2sS%2$`031-+h9KMZsIHfB>s@}>Y(z988e!`%4=EDoAQ0kbk>+lCoK60Mx9P!~I zlq~wf7kcm_NFImt3ZYlE(b3O1K^QWiFb$V^a2Jlwvm(!XYx<`i@ZMS3UwFt{;x+-v zhx{m=m;4dgvkKp5{*lfSN3o^keSpp9{hlXj%=}e_7Ou{Yiw(J@NXuh*;pL6@$HsfB zh?v+r^cp@jQ4EspC#RqpwPY(}_SS$wZ{S959`C25777&sgtNh%XTCo9VHJC-G z;;wi9{-iv+ETiY;K9qvlEc04f;ZnUP>cUL_T*ms``EtGoP^B#Q>n2dSrbAg8a>*Lg zd0EJ^=tdW~7fbcLFsqryFEcy*-8!?;n%;F+8i{eZyCDaiYxghr z$8k>L|2&-!lhvuVdk!r-kpSFl`5F5d4DJr%M4-qOy3gdmQbqF1=aBtRM7)c_Ae?$b8 zQg4c8*KQ{XJmL)1c7#0Yn0#PTMEs4-IHPjkn0!=;JdhMXqzMLeh`yOylXROP- zl#z3+fwM9l3%VN(6R77ua*uI9%hO7l7{+Hcbr(peh;afUK?B4EC09J{-u{mv)+u#? zdKVBCPt`eU@IzL)OXA`Ebu`Xp?u0m%h&X41}FNfnJ*g1!1wcbbpo%F4x!-#R9ft!8{5`Ho}04?FI#Kg zL|k`tF1t_`ywdy8(wnTut>HND(qNnq%Sq=AvvZbXnLx|mJhi!*&lwG2g|edBdVgLy zjvVTKHAx(+&P;P#2Xobo7_RttUi)Nllc}}hX>|N?-u5g7VJ-NNdwYcaOG?NK=5)}` zMtOL;o|i0mSKm(UI_7BL_^6HnVOTkuPI6y@ZLR(H?c1cr-_ouSLp{5!bx^DiKd*Yb z{K78Ci&Twup zTKm)ioN|wcYy%Qnwb)IzbH>W!;Ah5Zdm_jRY`+VRJ2 zhkspZ9hbK3iQD91A$d!0*-1i#%x81|s+SPRmD}d~<1p6!A13(!vABP2kNgqEG z?AMgl^P+iRoIY(9@_I?n1829lGvAsRnHwS~|5vD2+Zi53j<5N4wNn0{q>>jF9*bI) zL$kMXM-awNOElF>{?Jr^tOz1glbwaD-M0OKOlTeW3C!1ZyxRbB>8JDof(O&R1bh%3x#>y2~<>OXO#IIedH0Q`(&&?eo-c~ z>*Ah#3~09unym~UC-UFqqI>{dmUD$Y4@evG#ORLI*{ZM)Jl=e1it!XzY($S3V zLG!Y6fCjE>x6r@5FG1n|8ompSZaJ>9)q6jqU;XxCQk9zV(?C9+i*>w z21+KYt1gXX&0`x3E)hS7I5}snbBzox9C@Xzcr|{B8Hw;SY1$}&BoYKXH^hpjW-RgJ z-Fb}tannKCv>y~^`r|(1Q9;+sZlYf3XPSX|^gR01UFtu$B*R;$sPZdIZShRr>|b@J z;#G{EdoY+O;REEjQ}X7_YzWLO+Ey3>a_KDe1CjSe| z6arqcEZ)CX!8r(si`dqbF$uu&pnf^Np{1f*TdJ`r2;@SaZ z#hb4xlaCA@Pwqj#LlUEe5L{I$k(Zj$d3(~)u(F%&xb8={N9hKxlZIO1ABsM{Mt|)2 zJ^t9Id;?%4PfR4&Ph9B9cFK~@tG3wlFW-0fXZS_L4U*EiAA%+`h%q2^6BCC;t0iO4V=s4Qug{M|iDV@s zC7|ef-dxiR7T&Mpre!%hiUhHM%3Qxi$Lzw6&(Tvlx9QA_7LhYq<(o~=Y>3ka-zrQa zhGpfFK@)#)rtfz61w35^sN1=IFw&Oc!Nah+8@qhJ0UEGr;JplaxOGI82OVqZHsqfX ze1}r{jy;G?&}Da}a7>SCDsFDuzuseeCKof|Dz2BPsP8? zY;a)Tkr2P~0^2BeO?wnzF_Ul-ekY=-w26VnU%U3f19Z-pj&2 z4J_a|o4Dci+MO)mPQIM>kdPG1xydiR9@#8m zh27D7GF{p|a{8({Q-Pr-;#jV{2zHR>lGoFtIfIpoMo?exuQyX_A;;l0AP4!)JEM$EwMInZkj+8*IHP4vKRd zKx_l-i*>A*C@{u%ct`y~s6MWAfO{@FPIX&sg8H{GMDc{4M3%$@c8&RAlw0-R<4DO3 trJqdc$mBpWeznn?E0M$F`|3v=`3%T2A17h;rxP7$%JLd=6(2u;`(N3pt&so# diff --git a/site/index.html b/site/index.html deleted file mode 100644 index 230537bb5..000000000 --- a/site/index.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - Python Social Auth - - - - - - - - - -
          -
          -

          Python Social Auth

          -

          - Python Social Auth is an easy to setup social authentication/registration - mechanism with support for several frameworks and auth providers. -

          - -

          - Crafted using base code from django-social-auth, implements a common interface - to define new authentication providers from third parties. And to bring support - for more frameworks and ORMs. -

          -

          Learn more »

          -
          - -
          -
          -

          Frameworks

          -

          - The lib supports a few frameworks at the moment with Django, - Flask, Pyramid, - Webpy, CherryPy and - Tornado and more to come. The frameworks API - should ease the implementation to increase the number of frameworks supported. -

          -

          View details »

          -
          - -
          -

          Authentication Providers

          -

          - Ported from django-social-auth, the application - brings plenty of authentication providers, many from popular services like Google, - Facebook, Twitter and - Github. The backends API - have some implementation details on how to implement your own backends. -

          -

          View details »

          -
          -
          - -
          -
          -

          ORMs

          -

          - There are multiple ORM python libraries around, - some frameworks has their own built-in version too. python-social-auth - tries to support the different interfaces available, at the moment SQLAlchemy, - Django ORM and Mongoengine - are supported, but with the Storage API it should be easy to add more support. -

          -

          View details »

          -
          - -
          -

          Development and Contact

          -

          - The code is available on Github, report any - issue if you find any. Pull requests are - always welcome. There's a mailing list - and IRC channel #python-social-auth on Freenode network. -

          -

          View details »

          -
          -
          - -
          - -
          -

          © Matías Aguirre 2012

          -
          -
          - - - - - diff --git a/site/js/bootstrap.min.js b/site/js/bootstrap.min.js deleted file mode 100644 index e05923da8..000000000 --- a/site/js/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* Bootstrap.js by @fat & @mdo -* Copyright 2012 Twitter, Inc. -* http://www.apache.org/licenses/LICENSE-2.0.txt -*/ -!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
          ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

          '}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
        • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file diff --git a/social/__init__.py b/social/__init__.py index 8766fd5a1..0f1326029 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 2, 21) +version = (0, 3, 0) extra = '' __version__ = '.'.join(map(str, version)) + extra diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py index cb4fad670..7ebf69258 100644 --- a/social/apps/flask_app/peewee/models.py +++ b/social/apps/flask_app/peewee/models.py @@ -1 +1 @@ -fro social_flask_peewee.models import FlaskStorage, init_social +from social_flask_peewee.models import FlaskStorage, init_social diff --git a/tox.ini b/tox.ini index 0ddc92f13..f109b3d46 100644 --- a/tox.ini +++ b/tox.ini @@ -21,8 +21,3 @@ deps = -r{toxinidir}/social/tests/requirements-python3.txt [testenv:py35] deps = -r{toxinidir}/social/tests/requirements-python3.txt - -[testenv:doc] -changedir = docs -deps = sphinx -commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html From f97a91cb84f1178607217018854b14907b90b537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 3 Dec 2016 11:45:29 -0300 Subject: [PATCH 870/890] Migration document --- MIGRATING_TO_SOCIAL.md | 44 ++++++++++++++++++++++++++++++++++++++++++ README.rst | 4 ++++ 2 files changed, 48 insertions(+) create mode 100644 MIGRATING_TO_SOCIAL.md diff --git a/MIGRATING_TO_SOCIAL.md b/MIGRATING_TO_SOCIAL.md new file mode 100644 index 000000000..baf23d489 --- /dev/null +++ b/MIGRATING_TO_SOCIAL.md @@ -0,0 +1,44 @@ +# Migrating from python-social-auth to split social + +Since Dec 03 2016, [python-socia-auth](https://github.com/omab/python-social-auth) +is marked as deprecated and the community is recommended to migrate +towards the packages created in the [organization repository](https://github.com/python-social-auth/social-core). + +The new organization split the monolithic structure into smaller +packages with their responsibility well defined, and better +dependencies handling. + +Since [v0.3.0](https://github.com/omab/python-social-auth/tree/v0.3.0), +python-social-auth cleaned up the code and added the needed imports to +the new libraries and defined a single dependency in the [requirements.txt](https://github.com/omab/python-social-auth/blob/v0.3.0/requirements.txt) +file, `social-auth-core`, this aims to ease the transition to the new structure. + +But that won't solve everybody situation, people using the different +frameworks also need to define their corresponding requirement. + +## Django + +Django users need to add the `social-auth-app-django` +dependency. Those using `mongoengine`, need to add +`social-auth-app-mongoengine`. + +## Flask + +Flask users need to add `social-auth-app-flask`, and depending on the +storage solution, add one of the following too: + + - `social-auth-app-flask-sqlalchemy` when using SQLAlchemy + - `social-auth-app-flask-mongoengine` when using Mongoengine + - `social-auth-app-flask-peewee` when using Peewee + +## Pyramid + +Pyramid users need to add `social-auth-app-pyramid` to their dependencies. + +## Tornado + +Tornado users need to add `social-auth-app-tornado` to their dependencies. + +## Webpy + +Web.py users need to add `social-auth-app-webpy` to their dependencies. diff --git a/README.rst b/README.rst index 0ec3b88a7..56da0eb6a 100644 --- a/README.rst +++ b/README.rst @@ -15,4 +15,8 @@ As for Dec 03 2016, this library is now deprecated, the codebase was split and migrated into the `python-social-auth organization`_, where a more organized development process is expected to take place. +Details about moving towards the new setup is documented in the +`migrating to social` document. + .. _python-social-auth organization: https://github.com/python-social-auth +.. _migrating to social: https://github.com/omab/python-social-auth/blob/master/MIGRATING_TO_SOCIAL.md From a8a017bb6e9d69e6d96034b908d4cc054255cf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 3 Dec 2016 11:46:04 -0300 Subject: [PATCH 871/890] Fix migration link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 56da0eb6a..1abbf0046 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ split and migrated into the `python-social-auth organization`_, where a more organized development process is expected to take place. Details about moving towards the new setup is documented in the -`migrating to social` document. +`migrating to social`_ document. .. _python-social-auth organization: https://github.com/python-social-auth .. _migrating to social: https://github.com/omab/python-social-auth/blob/master/MIGRATING_TO_SOCIAL.md From c8ca15f504c8e1bfd58bf27cc45d77e64838bd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 3 Dec 2016 15:38:25 -0300 Subject: [PATCH 872/890] Remove deprecated examples --- examples/cherrypy_example/__init__.py | 77 --- examples/cherrypy_example/db/__init__.py | 4 - examples/cherrypy_example/db/saplugin.py | 39 -- examples/cherrypy_example/db/satool.py | 24 - examples/cherrypy_example/db/user.py | 16 - .../local_settings.py.template | 46 -- examples/cherrypy_example/requirements.txt | 1 - examples/cherrypy_example/syncbd.py | 24 - examples/cherrypy_example/templates/base.html | 14 - examples/cherrypy_example/templates/done.html | 24 - examples/cherrypy_example/templates/home.html | 84 ---- examples/django_example/example/__init__.py | 0 .../django_example/example/app/__init__.py | 0 .../django_example/example/app/decorators.py | 16 - examples/django_example/example/app/mail.py | 13 - examples/django_example/example/app/models.py | 6 - .../django_example/example/app/pipeline.py | 15 - .../example/app/templatetags/__init__.py | 0 .../example/app/templatetags/backend_utils.py | 81 --- examples/django_example/example/app/views.py | 74 --- examples/django_example/example/settings.py | 252 ---------- .../example/templates/home.html | 466 ------------------ examples/django_example/example/urls.py | 18 - examples/django_example/example/wsgi.py | 28 -- examples/django_example/manage.py | 8 - examples/django_example/requirements.txt | 2 - .../django_me_example/example/__init__.py | 0 .../django_me_example/example/app/__init__.py | 0 .../example/app/decorators.py | 16 - .../django_me_example/example/app/mail.py | 13 - .../django_me_example/example/app/models.py | 0 .../django_me_example/example/app/pipeline.py | 15 - .../example/app/templatetags/__init__.py | 0 .../example/app/templatetags/backend_utils.py | 82 --- .../django_me_example/example/app/views.py | 74 --- .../django_me_example/example/settings.py | 226 --------- .../example/templates/home.html | 440 ----------------- examples/django_me_example/example/urls.py | 18 - examples/django_me_example/example/wsgi.py | 28 -- examples/django_me_example/manage.py | 8 - examples/django_me_example/requirements.txt | 3 - examples/flask_example/__init__.py | 73 --- examples/flask_example/manage.py | 27 - examples/flask_example/models/__init__.py | 2 - examples/flask_example/models/user.py | 23 - examples/flask_example/requirements.txt | 6 - examples/flask_example/routes/__init__.py | 2 - examples/flask_example/routes/main.py | 22 - examples/flask_example/settings.py | 56 --- examples/flask_example/templates/base.html | 14 - examples/flask_example/templates/done.html | 24 - examples/flask_example/templates/home.html | 85 ---- examples/flask_me_example/__init__.py | 59 --- examples/flask_me_example/manage.py | 20 - examples/flask_me_example/models/__init__.py | 2 - examples/flask_me_example/models/user.py | 16 - examples/flask_me_example/requirements.txt | 7 - examples/flask_me_example/routes/__init__.py | 2 - examples/flask_me_example/routes/main.py | 22 - examples/flask_me_example/settings.py | 62 --- examples/flask_me_example/templates/base.html | 14 - examples/flask_me_example/templates/done.html | 24 - examples/flask_me_example/templates/home.html | 85 ---- examples/flask_peewee_example/__init__.py | 62 --- examples/flask_peewee_example/manage.py | 26 - .../flask_peewee_example/models/__init__.py | 4 - examples/flask_peewee_example/models/user.py | 24 - .../flask_peewee_example/requirements.txt | 7 - .../flask_peewee_example/routes/__init__.py | 2 - examples/flask_peewee_example/routes/main.py | 22 - examples/flask_peewee_example/settings.py | 55 --- .../flask_peewee_example/templates/base.html | 14 - .../flask_peewee_example/templates/done.html | 24 - .../flask_peewee_example/templates/home.html | 85 ---- examples/pyramid_example/CHANGES.txt | 4 - examples/pyramid_example/MANIFEST.in | 2 - examples/pyramid_example/README.txt | 14 - examples/pyramid_example/development.ini | 71 --- examples/pyramid_example/example/__init__.py | 34 -- examples/pyramid_example/example/auth.py | 30 -- .../example/local_settings.py.template | 95 ---- examples/pyramid_example/example/models.py | 19 - .../example/scripts/__init__.py | 1 - .../example/scripts/initializedb.py | 38 -- examples/pyramid_example/example/settings.py | 55 --- .../pyramid_example/example/templates/done.pt | 24 - .../pyramid_example/example/templates/home.pt | 91 ---- examples/pyramid_example/example/tests.py | 55 --- examples/pyramid_example/example/views.py | 11 - examples/pyramid_example/production.ini | 62 --- examples/pyramid_example/requirements.txt | 2 - examples/pyramid_example/setup.cfg | 27 - examples/pyramid_example/setup.py | 47 -- examples/tornado_example/__init__.py | 0 examples/tornado_example/app.py | 71 --- examples/tornado_example/models.py | 17 - examples/tornado_example/settings.py | 51 -- examples/tornado_example/templates/base.html | 14 - examples/tornado_example/templates/done.html | 5 - examples/tornado_example/templates/home.html | 85 ---- examples/webpy_example/__init__.py | 0 examples/webpy_example/app.py | 116 ----- examples/webpy_example/migrate.py | 8 - examples/webpy_example/models.py | 21 - examples/webpy_example/requirements.txt | 3 - examples/webpy_example/templates/base.html | 14 - examples/webpy_example/templates/done.html | 26 - examples/webpy_example/templates/home.html | 85 ---- 108 files changed, 4425 deletions(-) delete mode 100644 examples/cherrypy_example/__init__.py delete mode 100644 examples/cherrypy_example/db/__init__.py delete mode 100644 examples/cherrypy_example/db/saplugin.py delete mode 100644 examples/cherrypy_example/db/satool.py delete mode 100644 examples/cherrypy_example/db/user.py delete mode 100644 examples/cherrypy_example/local_settings.py.template delete mode 100644 examples/cherrypy_example/requirements.txt delete mode 100644 examples/cherrypy_example/syncbd.py delete mode 100644 examples/cherrypy_example/templates/base.html delete mode 100644 examples/cherrypy_example/templates/done.html delete mode 100644 examples/cherrypy_example/templates/home.html delete mode 100644 examples/django_example/example/__init__.py delete mode 100644 examples/django_example/example/app/__init__.py delete mode 100644 examples/django_example/example/app/decorators.py delete mode 100644 examples/django_example/example/app/mail.py delete mode 100644 examples/django_example/example/app/models.py delete mode 100644 examples/django_example/example/app/pipeline.py delete mode 100644 examples/django_example/example/app/templatetags/__init__.py delete mode 100644 examples/django_example/example/app/templatetags/backend_utils.py delete mode 100644 examples/django_example/example/app/views.py delete mode 100644 examples/django_example/example/settings.py delete mode 100644 examples/django_example/example/templates/home.html delete mode 100644 examples/django_example/example/urls.py delete mode 100644 examples/django_example/example/wsgi.py delete mode 100755 examples/django_example/manage.py delete mode 100644 examples/django_example/requirements.txt delete mode 100644 examples/django_me_example/example/__init__.py delete mode 100644 examples/django_me_example/example/app/__init__.py delete mode 100644 examples/django_me_example/example/app/decorators.py delete mode 100644 examples/django_me_example/example/app/mail.py delete mode 100644 examples/django_me_example/example/app/models.py delete mode 100644 examples/django_me_example/example/app/pipeline.py delete mode 100644 examples/django_me_example/example/app/templatetags/__init__.py delete mode 100644 examples/django_me_example/example/app/templatetags/backend_utils.py delete mode 100644 examples/django_me_example/example/app/views.py delete mode 100644 examples/django_me_example/example/settings.py delete mode 100644 examples/django_me_example/example/templates/home.html delete mode 100644 examples/django_me_example/example/urls.py delete mode 100644 examples/django_me_example/example/wsgi.py delete mode 100755 examples/django_me_example/manage.py delete mode 100644 examples/django_me_example/requirements.txt delete mode 100755 examples/flask_example/__init__.py delete mode 100755 examples/flask_example/manage.py delete mode 100644 examples/flask_example/models/__init__.py delete mode 100644 examples/flask_example/models/user.py delete mode 100644 examples/flask_example/requirements.txt delete mode 100644 examples/flask_example/routes/__init__.py delete mode 100644 examples/flask_example/routes/main.py delete mode 100644 examples/flask_example/settings.py delete mode 100644 examples/flask_example/templates/base.html delete mode 100644 examples/flask_example/templates/done.html delete mode 100644 examples/flask_example/templates/home.html delete mode 100644 examples/flask_me_example/__init__.py delete mode 100755 examples/flask_me_example/manage.py delete mode 100644 examples/flask_me_example/models/__init__.py delete mode 100644 examples/flask_me_example/models/user.py delete mode 100644 examples/flask_me_example/requirements.txt delete mode 100644 examples/flask_me_example/routes/__init__.py delete mode 100644 examples/flask_me_example/routes/main.py delete mode 100644 examples/flask_me_example/settings.py delete mode 100644 examples/flask_me_example/templates/base.html delete mode 100644 examples/flask_me_example/templates/done.html delete mode 100644 examples/flask_me_example/templates/home.html delete mode 100644 examples/flask_peewee_example/__init__.py delete mode 100644 examples/flask_peewee_example/manage.py delete mode 100644 examples/flask_peewee_example/models/__init__.py delete mode 100644 examples/flask_peewee_example/models/user.py delete mode 100644 examples/flask_peewee_example/requirements.txt delete mode 100644 examples/flask_peewee_example/routes/__init__.py delete mode 100644 examples/flask_peewee_example/routes/main.py delete mode 100644 examples/flask_peewee_example/settings.py delete mode 100644 examples/flask_peewee_example/templates/base.html delete mode 100644 examples/flask_peewee_example/templates/done.html delete mode 100644 examples/flask_peewee_example/templates/home.html delete mode 100644 examples/pyramid_example/CHANGES.txt delete mode 100644 examples/pyramid_example/MANIFEST.in delete mode 100644 examples/pyramid_example/README.txt delete mode 100644 examples/pyramid_example/development.ini delete mode 100644 examples/pyramid_example/example/__init__.py delete mode 100644 examples/pyramid_example/example/auth.py delete mode 100644 examples/pyramid_example/example/local_settings.py.template delete mode 100644 examples/pyramid_example/example/models.py delete mode 100644 examples/pyramid_example/example/scripts/__init__.py delete mode 100644 examples/pyramid_example/example/scripts/initializedb.py delete mode 100644 examples/pyramid_example/example/settings.py delete mode 100644 examples/pyramid_example/example/templates/done.pt delete mode 100644 examples/pyramid_example/example/templates/home.pt delete mode 100644 examples/pyramid_example/example/tests.py delete mode 100644 examples/pyramid_example/example/views.py delete mode 100644 examples/pyramid_example/production.ini delete mode 100644 examples/pyramid_example/requirements.txt delete mode 100644 examples/pyramid_example/setup.cfg delete mode 100644 examples/pyramid_example/setup.py delete mode 100644 examples/tornado_example/__init__.py delete mode 100644 examples/tornado_example/app.py delete mode 100644 examples/tornado_example/models.py delete mode 100644 examples/tornado_example/settings.py delete mode 100644 examples/tornado_example/templates/base.html delete mode 100644 examples/tornado_example/templates/done.html delete mode 100644 examples/tornado_example/templates/home.html delete mode 100644 examples/webpy_example/__init__.py delete mode 100644 examples/webpy_example/app.py delete mode 100644 examples/webpy_example/migrate.py delete mode 100644 examples/webpy_example/models.py delete mode 100644 examples/webpy_example/requirements.txt delete mode 100644 examples/webpy_example/templates/base.html delete mode 100644 examples/webpy_example/templates/done.html delete mode 100644 examples/webpy_example/templates/home.html diff --git a/examples/cherrypy_example/__init__.py b/examples/cherrypy_example/__init__.py deleted file mode 100644 index 2a69a81a0..000000000 --- a/examples/cherrypy_example/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -import sys - -sys.path.append('../..') - -import cherrypy - -from jinja2 import Environment, FileSystemLoader - -from social.apps.cherrypy_app.utils import backends -from social.apps.cherrypy_app.views import CherryPyPSAViews - -from db.saplugin import SAEnginePlugin -from db.satool import SATool -from db.user import User - - -SAEnginePlugin(cherrypy.engine, 'sqlite:///test.db').subscribe() - - -class PSAExample(CherryPyPSAViews): - @cherrypy.expose - def index(self): - return self.render_to('home.html') - - @cherrypy.expose - def done(self): - user = getattr(cherrypy.request, 'user', None) - if user is None: - raise cherrypy.HTTPRedirect('/') - return self.render_to('done.html', user=user, backends=backends(user)) - - @cherrypy.expose - def logout(self): - raise cherrypy.HTTPRedirect('/') - - def render_to(self, tpl, **ctx): - return cherrypy.tools.jinja2env.get_template(tpl).render(**ctx) - - -def load_user(): - user_id = cherrypy.session.get('user_id') - if user_id: - cherrypy.request.user = cherrypy.request.db.query(User).get(user_id) - else: - cherrypy.request.user = None - - -def session_commit(): - cherrypy.session.save() - - -try: - from local_settings import SOCIAL_SETTINGS -except ImportError: - print 'Define a local_settings.py using local_settings.py.template as base' - SOCIAL_SETTINGS = {} - - -if __name__ == '__main__': - cherrypy.config.update({ - 'server.socket_port': 8000, - 'tools.sessions.on': True, - 'tools.sessions.storage_type': 'ram', - 'tools.db.on': True, - 'tools.authenticate.on': True, - 'SOCIAL_AUTH_USER_MODEL': 'db.user.User', - 'SOCIAL_AUTH_LOGIN_URL': '/', - 'SOCIAL_AUTH_LOGIN_REDIRECT_URL': '/done', - }) - cherrypy.config.update(SOCIAL_SETTINGS) - cherrypy.tools.jinja2env = Environment( - loader=FileSystemLoader('templates') - ) - cherrypy.tools.db = SATool() - cherrypy.tools.authenticate = cherrypy.Tool('before_handler', load_user) - cherrypy.tools.session = cherrypy.Tool('on_end_resource', session_commit) - cherrypy.quickstart(PSAExample()) diff --git a/examples/cherrypy_example/db/__init__.py b/examples/cherrypy_example/db/__init__.py deleted file mode 100644 index 00ea8e136..000000000 --- a/examples/cherrypy_example/db/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from sqlalchemy.ext.declarative import declarative_base - - -Base = declarative_base() diff --git a/examples/cherrypy_example/db/saplugin.py b/examples/cherrypy_example/db/saplugin.py deleted file mode 100644 index 072e56d90..000000000 --- a/examples/cherrypy_example/db/saplugin.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -from cherrypy.process import plugins - -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker - - -class SAEnginePlugin(plugins.SimplePlugin): - def __init__(self, bus, connection_string=None): - self.sa_engine = None - self.connection_string = connection_string - self.session = scoped_session(sessionmaker(autoflush=True, - autocommit=False)) - super(SAEnginePlugin, self).__init__(bus) - - def start(self): - self.sa_engine = create_engine(self.connection_string, echo=False) - self.bus.subscribe('bind-session', self.bind) - self.bus.subscribe('commit-session', self.commit) - - def stop(self): - self.bus.unsubscribe('bind-session', self.bind) - self.bus.unsubscribe('commit-session', self.commit) - if self.sa_engine: - self.sa_engine.dispose() - self.sa_engine = None - - def bind(self): - self.session.configure(bind=self.sa_engine) - return self.session - - def commit(self): - try: - self.session.commit() - except: - self.session.rollback() - raise - finally: - self.session.remove() diff --git a/examples/cherrypy_example/db/satool.py b/examples/cherrypy_example/db/satool.py deleted file mode 100644 index 1795cd569..000000000 --- a/examples/cherrypy_example/db/satool.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -import cherrypy - - -class SATool(cherrypy.Tool): - def __init__(self): - super(SATool, self).__init__('before_handler', self.bind_session, - priority=20) - - def _setup(self): - super(SATool, self)._setup() - cherrypy.request.hooks.attach('on_end_resource', - self.commit_transaction, - priority=80) - - def bind_session(self): - session = cherrypy.engine.publish('bind-session').pop() - cherrypy.request.db = session - - def commit_transaction(self): - if not hasattr(cherrypy.request, 'db'): - return - cherrypy.request.db = None - cherrypy.engine.publish('commit-session') diff --git a/examples/cherrypy_example/db/user.py b/examples/cherrypy_example/db/user.py deleted file mode 100644 index 94e0c191e..000000000 --- a/examples/cherrypy_example/db/user.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean - -from db import Base - - -class User(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - username = Column(String(200)) - password = Column(String(200), default='') - name = Column(String(100)) - email = Column(String(200)) - active = Column(Boolean, default=True) - - def is_active(self): - return self.active diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template deleted file mode 100644 index 7209aca8d..000000000 --- a/examples/cherrypy_example/local_settings.py.template +++ /dev/null @@ -1,46 +0,0 @@ -SOCIAL_SETTINGS = { - 'SOCIAL_AUTH_AUTHENTICATION_BACKENDS': ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', - ), - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': '', - 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': '' -} diff --git a/examples/cherrypy_example/requirements.txt b/examples/cherrypy_example/requirements.txt deleted file mode 100644 index d71870692..000000000 --- a/examples/cherrypy_example/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -cherrypy diff --git a/examples/cherrypy_example/syncbd.py b/examples/cherrypy_example/syncbd.py deleted file mode 100644 index 50bb962eb..000000000 --- a/examples/cherrypy_example/syncbd.py +++ /dev/null @@ -1,24 +0,0 @@ -import sys - -sys.path.append('../..') - -from sqlalchemy import create_engine - -import cherrypy - - -cherrypy.config.update({ - 'SOCIAL_AUTH_USER_MODEL': 'db.user.User', -}) - - -from social.apps.cherrypy_app.models import SocialBase -from db import Base -from db.user import User - - - -if __name__ == '__main__': - engine = create_engine('sqlite:///test.db') - Base.metadata.create_all(engine) - SocialBase.metadata.create_all(engine) diff --git a/examples/cherrypy_example/templates/base.html b/examples/cherrypy_example/templates/base.html deleted file mode 100644 index 86db50440..000000000 --- a/examples/cherrypy_example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/cherrypy_example/templates/done.html b/examples/cherrypy_example/templates/done.html deleted file mode 100644 index 9fd7b60c8..000000000 --- a/examples/cherrypy_example/templates/done.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

          You are logged in as {{ user.username }}!

          - -

          Associated:

          -
            - {% for assoc in backends.associated %} -
          • - {{ assoc.provider }} -
            -
          • - {% endfor %} -
          - -

          Associate:

          -
            - {% for name in backends.not_associated %} -
          • - {{ name }} -
          • - {% endfor %} -
          -{% endblock %} diff --git a/examples/cherrypy_example/templates/home.html b/examples/cherrypy_example/templates/home.html deleted file mode 100644 index 91302d26d..000000000 --- a/examples/cherrypy_example/templates/home.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -ThisIsMyJam OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Podio OAuth2
          -MineID OAuth2
          - -
          -
          - - - -
          -
          - -
          -
          - - - -
          -
          - -
          - - Persona -
          -{% endblock %} - -{% block scripts %} - - - -{% endblock %} diff --git a/examples/django_example/example/__init__.py b/examples/django_example/example/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_example/example/app/__init__.py b/examples/django_example/example/app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_example/example/app/decorators.py b/examples/django_example/example/app/decorators.py deleted file mode 100644 index 2ba85b130..000000000 --- a/examples/django_example/example/app/decorators.py +++ /dev/null @@ -1,16 +0,0 @@ -from functools import wraps - -from django.template import RequestContext -from django.shortcuts import render_to_response - - -def render_to(tpl): - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - out = func(request, *args, **kwargs) - if isinstance(out, dict): - out = render_to_response(tpl, out, RequestContext(request)) - return out - return wrapper - return decorator diff --git a/examples/django_example/example/app/mail.py b/examples/django_example/example/app/mail.py deleted file mode 100644 index 4dd59b5a7..000000000 --- a/examples/django_example/example/app/mail.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.conf import settings -from django.core.mail import send_mail -from django.core.urlresolvers import reverse - - -def send_validation(strategy, backend, code): - url = '{0}?verification_code={1}'.format( - reverse('social:complete', args=(backend.name,)), - code.code - ) - url = strategy.request.build_absolute_uri(url) - send_mail('Validate your account', 'Validate your account {0}'.format(url), - settings.EMAIL_FROM, [code.email], fail_silently=False) diff --git a/examples/django_example/example/app/models.py b/examples/django_example/example/app/models.py deleted file mode 100644 index 415d74896..000000000 --- a/examples/django_example/example/app/models.py +++ /dev/null @@ -1,6 +0,0 @@ -# Define a custom User class to work with django-social-auth -from django.contrib.auth.models import AbstractUser - - -class CustomUser(AbstractUser): - pass diff --git a/examples/django_example/example/app/pipeline.py b/examples/django_example/example/app/pipeline.py deleted file mode 100644 index 245e69cf2..000000000 --- a/examples/django_example/example/app/pipeline.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.shortcuts import redirect - -from social.pipeline.partial import partial - - -@partial -def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): - if kwargs.get('ajax') or user and user.email: - return - elif is_new and not details.get('email'): - email = strategy.request_data().get('email') - if email: - details['email'] = email - else: - return redirect('require_email') diff --git a/examples/django_example/example/app/templatetags/__init__.py b/examples/django_example/example/app/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_example/example/app/templatetags/backend_utils.py b/examples/django_example/example/app/templatetags/backend_utils.py deleted file mode 100644 index 99abf72a8..000000000 --- a/examples/django_example/example/app/templatetags/backend_utils.py +++ /dev/null @@ -1,81 +0,0 @@ -import re - -from django import template - -from social.backends.oauth import OAuthAuth - - -register = template.Library() - -name_re = re.compile(r'([^O])Auth') - - -@register.filter -def backend_name(backend): - name = backend.__class__.__name__ - name = name.replace('OAuth', ' OAuth') - name = name.replace('OpenId', ' OpenId') - name = name.replace('Sandbox', '') - name = name_re.sub(r'\1 Auth', name) - return name - - -@register.filter -def backend_class(backend): - return backend.name.replace('-', ' ') - - -@register.filter -def icon_name(name): - return { - 'stackoverflow': 'stack-overflow', - 'google-oauth': 'google', - 'google-oauth2': 'google', - 'google-openidconnect': 'google', - 'yahoo-oauth': 'yahoo', - 'facebook-app': 'facebook', - 'email': 'envelope', - 'vimeo': 'vimeo-square', - 'linkedin-oauth2': 'linkedin', - 'vk-oauth2': 'vk', - 'live': 'windows', - 'username': 'user', - }.get(name, name) - - -@register.filter -def social_backends(backends): - backends = [(name, backend) for name, backend in backends.items() - if name not in ['username', 'email']] - backends.sort(key=lambda b: b[0]) - return [backends[n:n + 10] for n in range(0, len(backends), 10)] - - -@register.filter -def legacy_backends(backends): - backends = [(name, backend) for name, backend in backends.items() - if name in ['username', 'email']] - backends.sort(key=lambda b: b[0]) - return backends - - -@register.filter -def oauth_backends(backends): - backends = [(name, backend) for name, backend in backends.items() - if issubclass(backend, OAuthAuth)] - backends.sort(key=lambda b: b[0]) - return backends - - -@register.simple_tag(takes_context=True) -def associated(context, backend): - user = context.get('user') - context['association'] = None - if user and user.is_authenticated(): - try: - context['association'] = user.social_auth.filter( - provider=backend.name - )[0] - except IndexError: - pass - return '' diff --git a/examples/django_example/example/app/views.py b/examples/django_example/example/app/views.py deleted file mode 100644 index 2ed66540b..000000000 --- a/examples/django_example/example/app/views.py +++ /dev/null @@ -1,74 +0,0 @@ -import json - -from django.conf import settings -from django.http import HttpResponse, HttpResponseBadRequest -from django.shortcuts import redirect -from django.contrib.auth.decorators import login_required -from django.contrib.auth import logout as auth_logout, login - -from social.backends.oauth import BaseOAuth1, BaseOAuth2 -from social.backends.google import GooglePlusAuth -from social.backends.utils import load_backends -from social.apps.django_app.utils import psa - -from example.app.decorators import render_to - - -def logout(request): - """Logs out user""" - auth_logout(request) - return redirect('/') - - -def context(**extra): - return dict({ - 'plus_id': getattr(settings, 'SOCIAL_AUTH_GOOGLE_PLUS_KEY', None), - 'plus_scope': ' '.join(GooglePlusAuth.DEFAULT_SCOPE), - 'available_backends': load_backends(settings.AUTHENTICATION_BACKENDS) - }, **extra) - - -@render_to('home.html') -def home(request): - """Home view, displays login mechanism""" - if request.user.is_authenticated(): - return redirect('done') - return context() - - -@login_required -@render_to('home.html') -def done(request): - """Login complete view, displays user data""" - return context() - - -@render_to('home.html') -def validation_sent(request): - return context( - validation_sent=True, - email=request.session.get('email_validation_address') - ) - - -@render_to('home.html') -def require_email(request): - backend = request.session['partial_pipeline']['backend'] - return context(email_required=True, backend=backend) - - -@psa('social:complete') -def ajax_auth(request, backend): - if isinstance(request.backend, BaseOAuth1): - token = { - 'oauth_token': request.REQUEST.get('access_token'), - 'oauth_token_secret': request.REQUEST.get('access_token_secret'), - } - elif isinstance(request.backend, BaseOAuth2): - token = request.REQUEST.get('access_token') - else: - raise HttpResponseBadRequest('Wrong backend type') - user = request.backend.do_auth(token, ajax=True) - login(request, user) - data = {'id': user.id, 'username': user.username} - return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py deleted file mode 100644 index 9077b5a42..000000000 --- a/examples/django_example/example/settings.py +++ /dev/null @@ -1,252 +0,0 @@ -import sys -from os.path import abspath, dirname, join - - -sys.path.insert(0, '../..') - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ROOT_PATH = abspath(dirname(__file__)) - -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'test.db' - } -} - -TIME_ZONE = 'America/Montevideo' -LANGUAGE_CODE = 'en-us' -SITE_ID = 1 -USE_I18N = True -USE_L10N = True -USE_TZ = True -MEDIA_ROOT = '' -MEDIA_URL = '' - -STATIC_ROOT = '' -STATIC_URL = '/static/' -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -SECRET_KEY = '#$5btppqih8=%ae^#&7en#kyi!vh%he9rg=ed#hm6fnw9^=umc' - -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'example.urls' - -# Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'example.wsgi.application' - -TEMPLATE_DIRS = ( - join(ROOT_PATH, 'templates'), -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.admin', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'social.apps.django_app.default', - 'example.app', -) - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' - -TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.contrib.auth.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.contrib.messages.context_processors.messages', - 'social.apps.django_app.context_processors.backends', -) - -AUTHENTICATION_BACKENDS = ( - 'social.backends.amazon.AmazonOAuth2', - 'social.backends.angel.AngelOAuth2', - 'social.backends.aol.AOLOpenId', - 'social.backends.appsfuel.AppsfuelOAuth2', - 'social.backends.beats.BeatsOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.belgiumeid.BelgiumEIDOpenId', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.clef.ClefOAuth2', - 'social.backends.coinbase.CoinbaseOAuth2', - 'social.backends.coursera.CourseraOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.deezer.DeezerOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.douban.DoubanOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.dropbox.DropboxOAuth2', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.fedora.FedoraOpenId', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.github.GithubOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GooglePlusAuth', - 'social.backends.google.GoogleOpenIdConnect', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.jawbone.JawboneOAuth2', - 'social.backends.kakao.KakaoOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.linkedin.LinkedinOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.mailru.MailruOAuth2', - 'social.backends.mendeley.MendeleyOAuth', - 'social.backends.mendeley.MendeleyOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.mixcloud.MixcloudOAuth2', - 'social.backends.nationbuilder.NationBuilderOAuth2', - 'social.backends.odnoklassniki.OdnoklassnikiOAuth2', - 'social.backends.open_id.OpenIdAuth', - 'social.backends.openstreetmap.OpenStreetMapOAuth', - 'social.backends.persona.PersonaAuth', - 'social.backends.podio.PodioOAuth2', - 'social.backends.rdio.RdioOAuth1', - 'social.backends.rdio.RdioOAuth2', - 'social.backends.readability.ReadabilityOAuth', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.runkeeper.RunKeeperOAuth2', - 'social.backends.sketchfab.SketchfabOAuth2', - 'social.backends.skyrock.SkyrockOAuth', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.spotify.SpotifyOAuth2', - 'social.backends.stackoverflow.StackoverflowOAuth2', - 'social.backends.steam.SteamOpenId', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.suse.OpenSUSEOpenId', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.trello.TrelloOAuth', - 'social.backends.tripit.TripItOAuth', - 'social.backends.tumblr.TumblrOAuth', - 'social.backends.twilio.TwilioAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.vk.VKOAuth2', - 'social.backends.weibo.WeiboOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.xing.XingOAuth', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.yammer.YammerOAuth2', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.vimeo.VimeoOAuth1', - 'social.backends.lastfm.LastFmAuth', - 'social.backends.moves.MovesOAuth2', - 'social.backends.vend.VendOAuth2', - 'social.backends.email.EmailAuth', - 'social.backends.username.UsernameAuth', - 'django.contrib.auth.backends.ModelBackend', - 'social.backends.upwork.UpworkOAuth', -) - -AUTH_USER_MODEL = 'app.CustomUser' - -LOGIN_URL = '/login/' -LOGIN_REDIRECT_URL = '/done/' -URL_PATH = '' -SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' -SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage' -SOCIAL_AUTH_GOOGLE_OAUTH_SCOPE = [ - 'https://www.googleapis.com/auth/drive', - 'https://www.googleapis.com/auth/userinfo.profile' -] -# SOCIAL_AUTH_EMAIL_FORM_URL = '/signup-email' -SOCIAL_AUTH_EMAIL_FORM_HTML = 'email_signup.html' -SOCIAL_AUTH_EMAIL_VALIDATION_FUNCTION = 'example.app.mail.send_validation' -SOCIAL_AUTH_EMAIL_VALIDATION_URL = '/email-sent/' -# SOCIAL_AUTH_USERNAME_FORM_URL = '/signup-username' -SOCIAL_AUTH_USERNAME_FORM_HTML = 'username_signup.html' - -SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', - 'example.app.pipeline.require_email', - 'social.pipeline.mail.mail_validation', - 'social.pipeline.user.create_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.debug.debug', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', - 'social.pipeline.debug.debug' -) - -TEST_RUNNER = 'django.test.runner.DiscoverRunner' - -# SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['first_name', 'last_name', 'email', -# 'username'] - -try: - from example.local_settings import * -except ImportError: - pass diff --git a/examples/django_example/example/templates/home.html b/examples/django_example/example/templates/home.html deleted file mode 100644 index 58b7ec8fb..000000000 --- a/examples/django_example/example/templates/home.html +++ /dev/null @@ -1,466 +0,0 @@ -{% load backend_utils %} - - - - Python Social Auth - - - - - - -

          Python Social Auth

          - -
          - {% if user.is_authenticated %} -
          - You are logged in as {{ user.username }}! -
          - {% endif %} - - - -
          - {% for name, backend in available_backends|legacy_backends %} - {% associated backend %} - {% if association %} -
          {% csrf_token %} - - - Disconnect {{ backend|backend_name }} - -
          - {% else %} - - - {{ backend|backend_name }} - - {% endif %} - {% endfor %} - - - - Ajax - -
          - - -
          - - - - - - - - - - - - - - {% if backend %} - - {% endif %} - - - - {% if plus_id %} - - - {% endif %} - - - - - - - - diff --git a/examples/django_example/example/urls.py b/examples/django_example/example/urls.py deleted file mode 100644 index 354ab4a5d..000000000 --- a/examples/django_example/example/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.conf.urls import patterns, include, url -from django.contrib import admin - - -admin.autodiscover() - -urlpatterns = patterns('', - url(r'^$', 'example.app.views.home'), - url(r'^admin/', include(admin.site.urls)), - url(r'^email-sent/', 'example.app.views.validation_sent'), - url(r'^login/$', 'example.app.views.home'), - url(r'^logout/$', 'example.app.views.logout'), - url(r'^done/$', 'example.app.views.done', name='done'), - url(r'^ajax-auth/(?P[^/]+)/$', 'example.app.views.ajax_auth', - name='ajax-auth'), - url(r'^email/$', 'example.app.views.require_email', name='require_email'), - url(r'', include('social.apps.django_app.urls', namespace='social')) -) diff --git a/examples/django_example/example/wsgi.py b/examples/django_example/example/wsgi.py deleted file mode 100644 index 4b3fb450d..000000000 --- a/examples/django_example/example/wsgi.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -WSGI config for dj project. - -This module contains the WSGI application used by Django's development server -and any production WSGI deployments. It should expose a module-level variable -named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover -this application via the ``WSGI_APPLICATION`` setting. - -Usually you will have the standard Django WSGI application here, but it also -might make sense to replace the whole Django WSGI application with a custom one -that later delegates to the Django one. For example, you could introduce WSGI -middleware here, or combine a Django application with an application of another -framework. - -""" -import os - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") - -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() - -# Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) diff --git a/examples/django_example/manage.py b/examples/django_example/manage.py deleted file mode 100755 index d49be0d4c..000000000 --- a/examples/django_example/manage.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/examples/django_example/requirements.txt b/examples/django_example/requirements.txt deleted file mode 100644 index 6453a5a1d..000000000 --- a/examples/django_example/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -django>=1.4 -python-social-auth diff --git a/examples/django_me_example/example/__init__.py b/examples/django_me_example/example/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_me_example/example/app/__init__.py b/examples/django_me_example/example/app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_me_example/example/app/decorators.py b/examples/django_me_example/example/app/decorators.py deleted file mode 100644 index 2ba85b130..000000000 --- a/examples/django_me_example/example/app/decorators.py +++ /dev/null @@ -1,16 +0,0 @@ -from functools import wraps - -from django.template import RequestContext -from django.shortcuts import render_to_response - - -def render_to(tpl): - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - out = func(request, *args, **kwargs) - if isinstance(out, dict): - out = render_to_response(tpl, out, RequestContext(request)) - return out - return wrapper - return decorator diff --git a/examples/django_me_example/example/app/mail.py b/examples/django_me_example/example/app/mail.py deleted file mode 100644 index 4dd59b5a7..000000000 --- a/examples/django_me_example/example/app/mail.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.conf import settings -from django.core.mail import send_mail -from django.core.urlresolvers import reverse - - -def send_validation(strategy, backend, code): - url = '{0}?verification_code={1}'.format( - reverse('social:complete', args=(backend.name,)), - code.code - ) - url = strategy.request.build_absolute_uri(url) - send_mail('Validate your account', 'Validate your account {0}'.format(url), - settings.EMAIL_FROM, [code.email], fail_silently=False) diff --git a/examples/django_me_example/example/app/models.py b/examples/django_me_example/example/app/models.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_me_example/example/app/pipeline.py b/examples/django_me_example/example/app/pipeline.py deleted file mode 100644 index 245e69cf2..000000000 --- a/examples/django_me_example/example/app/pipeline.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.shortcuts import redirect - -from social.pipeline.partial import partial - - -@partial -def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): - if kwargs.get('ajax') or user and user.email: - return - elif is_new and not details.get('email'): - email = strategy.request_data().get('email') - if email: - details['email'] = email - else: - return redirect('require_email') diff --git a/examples/django_me_example/example/app/templatetags/__init__.py b/examples/django_me_example/example/app/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/django_me_example/example/app/templatetags/backend_utils.py b/examples/django_me_example/example/app/templatetags/backend_utils.py deleted file mode 100644 index 573b6d637..000000000 --- a/examples/django_me_example/example/app/templatetags/backend_utils.py +++ /dev/null @@ -1,82 +0,0 @@ -import re - -from django import template - -from social.backends.oauth import OAuthAuth -from social.apps.django_app.me.models import UserSocialAuth - - -register = template.Library() - -name_re = re.compile(r'([^O])Auth') - - -@register.filter -def backend_name(backend): - name = backend.__class__.__name__ - name = name.replace('OAuth', ' OAuth') - name = name.replace('OpenId', ' OpenId') - name = name.replace('Sandbox', '') - name = name_re.sub(r'\1 Auth', name) - return name - - -@register.filter -def backend_class(backend): - return backend.name.replace('-', ' ') - - -@register.filter -def icon_name(name): - return { - 'stackoverflow': 'stack-overflow', - 'google-oauth': 'google', - 'google-oauth2': 'google', - 'google-openidconnect': 'google', - 'yahoo-oauth': 'yahoo', - 'facebook-app': 'facebook', - 'email': 'envelope', - 'vimeo': 'vimeo-square', - 'linkedin-oauth2': 'linkedin', - 'vk-oauth2': 'vk', - 'live': 'windows', - 'username': 'user', - }.get(name, name) - - -@register.filter -def social_backends(backends): - backends = [(name, backend) for name, backend in backends.items() - if name not in ['username', 'email']] - backends.sort(key=lambda b: b[0]) - return [backends[n:n + 10] for n in range(0, len(backends), 10)] - - -@register.filter -def legacy_backends(backends): - backends = [(name, backend) for name, backend in backends.items() - if name in ['username', 'email']] - backends.sort(key=lambda b: b[0]) - return backends - - -@register.filter -def oauth_backends(backends): - backends = [(name, backend) for name, backend in backends.items() - if issubclass(backend, OAuthAuth)] - backends.sort(key=lambda b: b[0]) - return backends - - -@register.simple_tag(takes_context=True) -def associated(context, backend): - user = context.get('user') - context['association'] = None - if user and user.is_authenticated(): - try: - context['association'] = UserSocialAuth.objects.filter( - user=user, provider=backend.name - )[0] - except IndexError: - pass - return '' diff --git a/examples/django_me_example/example/app/views.py b/examples/django_me_example/example/app/views.py deleted file mode 100644 index 2ed66540b..000000000 --- a/examples/django_me_example/example/app/views.py +++ /dev/null @@ -1,74 +0,0 @@ -import json - -from django.conf import settings -from django.http import HttpResponse, HttpResponseBadRequest -from django.shortcuts import redirect -from django.contrib.auth.decorators import login_required -from django.contrib.auth import logout as auth_logout, login - -from social.backends.oauth import BaseOAuth1, BaseOAuth2 -from social.backends.google import GooglePlusAuth -from social.backends.utils import load_backends -from social.apps.django_app.utils import psa - -from example.app.decorators import render_to - - -def logout(request): - """Logs out user""" - auth_logout(request) - return redirect('/') - - -def context(**extra): - return dict({ - 'plus_id': getattr(settings, 'SOCIAL_AUTH_GOOGLE_PLUS_KEY', None), - 'plus_scope': ' '.join(GooglePlusAuth.DEFAULT_SCOPE), - 'available_backends': load_backends(settings.AUTHENTICATION_BACKENDS) - }, **extra) - - -@render_to('home.html') -def home(request): - """Home view, displays login mechanism""" - if request.user.is_authenticated(): - return redirect('done') - return context() - - -@login_required -@render_to('home.html') -def done(request): - """Login complete view, displays user data""" - return context() - - -@render_to('home.html') -def validation_sent(request): - return context( - validation_sent=True, - email=request.session.get('email_validation_address') - ) - - -@render_to('home.html') -def require_email(request): - backend = request.session['partial_pipeline']['backend'] - return context(email_required=True, backend=backend) - - -@psa('social:complete') -def ajax_auth(request, backend): - if isinstance(request.backend, BaseOAuth1): - token = { - 'oauth_token': request.REQUEST.get('access_token'), - 'oauth_token_secret': request.REQUEST.get('access_token_secret'), - } - elif isinstance(request.backend, BaseOAuth2): - token = request.REQUEST.get('access_token') - else: - raise HttpResponseBadRequest('Wrong backend type') - user = request.backend.do_auth(token, ajax=True) - login(request, user) - data = {'id': user.id, 'username': user.username} - return HttpResponse(json.dumps(data), mimetype='application/json') diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py deleted file mode 100644 index 88783b3f4..000000000 --- a/examples/django_me_example/example/settings.py +++ /dev/null @@ -1,226 +0,0 @@ -import sys -from os.path import abspath, dirname, join - -import mongoengine - - -sys.path.insert(0, '../..') - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ROOT_PATH = abspath(dirname(__file__)) - -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.dummy', - } -} - -TIME_ZONE = 'America/Montevideo' -LANGUAGE_CODE = 'en-us' -SITE_ID = 1 -USE_I18N = True -USE_L10N = True -USE_TZ = True -MEDIA_ROOT = '' -MEDIA_URL = '' - -STATIC_ROOT = '' -STATIC_URL = '/static/' -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -SECRET_KEY = '#$5btppqih8=%ae^#&7en#kyi!vh%he9rg=ed#hm6fnw9^=umc' - -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'example.urls' - -# Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'example.wsgi.application' - -TEMPLATE_DIRS = ( - join(ROOT_PATH, 'templates'), -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.admin', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'mongoengine.django.mongo_auth', - 'social.apps.django_app.me', - 'example.app', -) - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.contrib.auth.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.contrib.messages.context_processors.messages', - 'social.apps.django_app.context_processors.backends', -) - -AUTH_USER_MODEL = 'mongo_auth.MongoUser' -MONGOENGINE_USER_DOCUMENT = 'mongoengine.django.auth.User' -SESSION_ENGINE = 'mongoengine.django.sessions' -SESSION_SERIALIZER = 'mongoengine.django.sessions.BSONSerializer' -mongoengine.connect('psa', host='mongodb://localhost/psa') -# MONGOENGINE_USER_DOCUMENT = 'example.app.models.User' -# SOCIAL_AUTH_USER_MODEL = 'example.app.models.User' - -AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.linkedin.LinkedinOAuth2', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.twilio.TwilioAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.douban.DoubanOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.mixcloud.MixcloudOAuth2', - 'social.backends.rdio.RdioOAuth1', - 'social.backends.rdio.RdioOAuth2', - 'social.backends.yammer.YammerOAuth2', - 'social.backends.stackoverflow.StackoverflowOAuth2', - 'social.backends.readability.ReadabilityOAuth', - 'social.backends.sketchfab.SketchfabOAuth2', - 'social.backends.skyrock.SkyrockOAuth', - 'social.backends.tumblr.TumblrOAuth', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.steam.SteamOpenId', - 'social.backends.podio.PodioOAuth2', - 'social.backends.amazon.AmazonOAuth2', - 'social.backends.email.EmailAuth', - 'social.backends.username.UsernameAuth', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', - 'mongoengine.django.auth.MongoEngineBackend', - 'django.contrib.auth.backends.ModelBackend', -) - -LOGIN_URL = '/login/' -LOGIN_REDIRECT_URL = '/done/' -URL_PATH = '' -SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy' -SOCIAL_AUTH_STORAGE = 'social.apps.django_app.me.models.DjangoStorage' -SOCIAL_AUTH_GOOGLE_OAUTH_SCOPE = [ - 'https://www.googleapis.com/auth/drive', - 'https://www.googleapis.com/auth/userinfo.profile' -] -# SOCIAL_AUTH_EMAIL_FORM_URL = '/signup-email' -SOCIAL_AUTH_EMAIL_FORM_HTML = 'email_signup.html' -SOCIAL_AUTH_EMAIL_VALIDATION_FUNCTION = 'example.app.mail.send_validation' -SOCIAL_AUTH_EMAIL_VALIDATION_URL = '/email-sent/' -# SOCIAL_AUTH_USERNAME_FORM_URL = '/signup-username' -SOCIAL_AUTH_USERNAME_FORM_HTML = 'username_signup.html' - -SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', - 'example.app.pipeline.require_email', - 'social.pipeline.mail.mail_validation', - 'social.pipeline.user.create_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details' -) - -TEST_RUNNER = 'django.test.runner.DiscoverRunner' - -try: - from example.local_settings import * -except ImportError: - pass diff --git a/examples/django_me_example/example/templates/home.html b/examples/django_me_example/example/templates/home.html deleted file mode 100644 index 3a51c5992..000000000 --- a/examples/django_me_example/example/templates/home.html +++ /dev/null @@ -1,440 +0,0 @@ -{% load backend_utils %} - - - - Python Social Auth - - - - - - -

          Python Social Auth

          - -
          - {% if user.is_authenticated %} -
          - You are logged in as {{ user.username }}! -
          - {% endif %} - - - -
          - {% for name, backend in available_backends|legacy_backends %} - {% associated backend %} - {% if association %} -
          {% csrf_token %} - - - Disconnect {{ backend|backend_name }} - -
          - {% else %} - - - {{ backend|backend_name }} - - {% endif %} - {% endfor %} - - - - Ajax - -
          - - -
          - - - - - - - - - - - - - - {% if backend %} - - {% endif %} - - - - {% if plus_id %} -
          {% csrf_token %} - - - -
          - -
          -
          - {% endif %} - - - - - - - - - diff --git a/examples/django_me_example/example/urls.py b/examples/django_me_example/example/urls.py deleted file mode 100644 index 354ab4a5d..000000000 --- a/examples/django_me_example/example/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.conf.urls import patterns, include, url -from django.contrib import admin - - -admin.autodiscover() - -urlpatterns = patterns('', - url(r'^$', 'example.app.views.home'), - url(r'^admin/', include(admin.site.urls)), - url(r'^email-sent/', 'example.app.views.validation_sent'), - url(r'^login/$', 'example.app.views.home'), - url(r'^logout/$', 'example.app.views.logout'), - url(r'^done/$', 'example.app.views.done', name='done'), - url(r'^ajax-auth/(?P[^/]+)/$', 'example.app.views.ajax_auth', - name='ajax-auth'), - url(r'^email/$', 'example.app.views.require_email', name='require_email'), - url(r'', include('social.apps.django_app.urls', namespace='social')) -) diff --git a/examples/django_me_example/example/wsgi.py b/examples/django_me_example/example/wsgi.py deleted file mode 100644 index 4b3fb450d..000000000 --- a/examples/django_me_example/example/wsgi.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -WSGI config for dj project. - -This module contains the WSGI application used by Django's development server -and any production WSGI deployments. It should expose a module-level variable -named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover -this application via the ``WSGI_APPLICATION`` setting. - -Usually you will have the standard Django WSGI application here, but it also -might make sense to replace the whole Django WSGI application with a custom one -that later delegates to the Django one. For example, you could introduce WSGI -middleware here, or combine a Django application with an application of another -framework. - -""" -import os - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") - -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() - -# Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) diff --git a/examples/django_me_example/manage.py b/examples/django_me_example/manage.py deleted file mode 100755 index d49be0d4c..000000000 --- a/examples/django_me_example/manage.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/examples/django_me_example/requirements.txt b/examples/django_me_example/requirements.txt deleted file mode 100644 index 1a339e997..000000000 --- a/examples/django_me_example/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -django>=1.4,<1.8 -mongoengine>=0.8.6 -python-social-auth diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py deleted file mode 100755 index 07c108083..000000000 --- a/examples/flask_example/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -import sys - -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker - -from flask import Flask, g -from flask_login import LoginManager, current_user - -sys.path.append('../..') - -from social.apps.flask_app.routes import social_auth -from social.apps.flask_app.template_filters import backends -from social.apps.flask_app.default.models import init_social - -# App -app = Flask(__name__) -app.config.from_object('flask_example.settings') - -try: - app.config.from_object('flask_example.local_settings') -except ImportError: - pass - -# DB -engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI']) -Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) -db_session = scoped_session(Session) - -app.register_blueprint(social_auth) -init_social(app, db_session) - -login_manager = LoginManager() -login_manager.login_view = 'main' -login_manager.login_message = '' -login_manager.init_app(app) - -from flask_example import models -from flask_example import routes - - -@login_manager.user_loader -def load_user(userid): - try: - return models.user.User.query.get(int(userid)) - except (TypeError, ValueError): - pass - - -@app.before_request -def global_user(): - # evaluate proxy value - g.user = current_user._get_current_object() - - -@app.teardown_appcontext -def commit_on_success(error=None): - if error is None: - db_session.commit() - else: - db_session.rollback() - - db_session.remove() - - -@app.context_processor -def inject_user(): - try: - return {'user': g.user} - except AttributeError: - return {'user': None} - - -app.context_processor(backends) diff --git a/examples/flask_example/manage.py b/examples/flask_example/manage.py deleted file mode 100755 index a298e0022..000000000 --- a/examples/flask_example/manage.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -import sys - -from flask.ext.script import Server, Manager, Shell - -sys.path.append('..') - -from flask_example import app, db_session, engine - - -manager = Manager(app) -manager.add_command('runserver', Server()) -manager.add_command('shell', Shell(make_context=lambda: { - 'app': app, - 'db_session': db_session -})) - - -@manager.command -def syncdb(): - from flask_example.models import user - from social.apps.flask_app.default import models - user.Base.metadata.create_all(engine) - models.PSABase.metadata.create_all(engine) - -if __name__ == '__main__': - manager.run() diff --git a/examples/flask_example/models/__init__.py b/examples/flask_example/models/__init__.py deleted file mode 100644 index e4adb9daf..000000000 --- a/examples/flask_example/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask_example.models import user -from social.apps.flask_app.default import models diff --git a/examples/flask_example/models/user.py b/examples/flask_example/models/user.py deleted file mode 100644 index 08bcee8da..000000000 --- a/examples/flask_example/models/user.py +++ /dev/null @@ -1,23 +0,0 @@ -from sqlalchemy import Column, String, Integer, Boolean -from sqlalchemy.ext.declarative import declarative_base - -from flask_login import UserMixin - -from flask_example import db_session - - -Base = declarative_base() -Base.query = db_session.query_property() - - -class User(Base, UserMixin): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - username = Column(String(200)) - password = Column(String(200), default='') - name = Column(String(100)) - email = Column(String(200)) - active = Column(Boolean, default=True) - - def is_active(self): - return self.active diff --git a/examples/flask_example/requirements.txt b/examples/flask_example/requirements.txt deleted file mode 100644 index 418fc8650..000000000 --- a/examples/flask_example/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask -Flask-Login -Flask-Script -Werkzeug -pysqlite -Jinja2 diff --git a/examples/flask_example/routes/__init__.py b/examples/flask_example/routes/__init__.py deleted file mode 100644 index d3586e81b..000000000 --- a/examples/flask_example/routes/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask_example.routes import main -from social.apps.flask_app import routes diff --git a/examples/flask_example/routes/main.py b/examples/flask_example/routes/main.py deleted file mode 100644 index fd6526185..000000000 --- a/examples/flask_example/routes/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import render_template, redirect -from flask_login import login_required, logout_user - -from flask_example import app - - -@app.route('/') -def main(): - return render_template('home.html') - - -@login_required -@app.route('/done/') -def done(): - return render_template('done.html') - - -@app.route('/logout') -def logout(): - """Logout view""" - logout_user() - return redirect('/') diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py deleted file mode 100644 index 6b5324cc5..000000000 --- a/examples/flask_example/settings.py +++ /dev/null @@ -1,56 +0,0 @@ -from os.path import dirname, abspath - -SECRET_KEY = 'random-secret-key' -SESSION_COOKIE_NAME = 'psa_session' -DEBUG = True -SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/test.db' % dirname(abspath(__file__)) -DEBUG_TB_INTERCEPT_REDIRECTS = False -SESSION_PROTECTION = 'strong' - -SOCIAL_AUTH_LOGIN_URL = '/' -SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/' -SOCIAL_AUTH_USER_MODEL = 'flask_example.models.user.User' -SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', -) diff --git a/examples/flask_example/templates/base.html b/examples/flask_example/templates/base.html deleted file mode 100644 index 86db50440..000000000 --- a/examples/flask_example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/flask_example/templates/done.html b/examples/flask_example/templates/done.html deleted file mode 100644 index ccabf53ec..000000000 --- a/examples/flask_example/templates/done.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

          You are logged in as {{ user.username }}!

          - -

          Associated:

          -{% for assoc in backends.associated %} -
          - {{ assoc.provider }} -
          - -
          -
          -{% endfor %} - -

          Associate:

          -
            - {% for name in backends.not_associated %} -
          • - {{ name }} -
          • - {% endfor %} -
          -{% endblock %} diff --git a/examples/flask_example/templates/home.html b/examples/flask_example/templates/home.html deleted file mode 100644 index 1c7f9bcef..000000000 --- a/examples/flask_example/templates/home.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -ThisIsMyJam OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Clef OAuth2
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Podio OAuth2
          -MineID OAuth2
          - -
          -
          - - - -
          -
          - -
          -
          - - - -
          -
          - -
          - - Persona -
          -{% endblock %} - -{% block scripts %} - - - -{% endblock %} diff --git a/examples/flask_me_example/__init__.py b/examples/flask_me_example/__init__.py deleted file mode 100644 index 9f5aaacb3..000000000 --- a/examples/flask_me_example/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -import sys - -from flask import Flask, g -from flask_login import LoginManager, current_user -from flask.ext.mongoengine import MongoEngine - -sys.path.append('../..') - -from social.apps.flask_app.routes import social_auth -from social.apps.flask_app.me.models import init_social -from social.apps.flask_app.template_filters import backends - - -# App -app = Flask(__name__) -app.config.from_object('flask_me_example.settings') -app.debug = True - -try: - app.config.from_object('flask_me_example.local_settings') -except ImportError: - pass - -# DB -db = MongoEngine(app) -app.register_blueprint(social_auth) -init_social(app, db) - -login_manager = LoginManager() -login_manager.login_view = 'main' -login_manager.login_message = '' -login_manager.init_app(app) - -from flask_me_example import models -from flask_me_example import routes - - -@login_manager.user_loader -def load_user(userid): - try: - return models.user.User.objects.get(id=userid) - except (TypeError, ValueError): - pass - - -@app.before_request -def global_user(): - g.user = current_user - - -@app.context_processor -def inject_user(): - try: - return {'user': g.user} - except AttributeError: - return {'user': None} - - -app.context_processor(backends) diff --git a/examples/flask_me_example/manage.py b/examples/flask_me_example/manage.py deleted file mode 100755 index b8948ef15..000000000 --- a/examples/flask_me_example/manage.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -import sys - -from flask.ext.script import Server, Manager, Shell - -sys.path.append('..') - -from flask_me_example import app, db - - -manager = Manager(app) -manager.add_command('runserver', Server()) -manager.add_command('shell', Shell(make_context=lambda: { - 'app': app, - 'db': db -})) - - -if __name__ == '__main__': - manager.run() diff --git a/examples/flask_me_example/models/__init__.py b/examples/flask_me_example/models/__init__.py deleted file mode 100644 index 6020112c3..000000000 --- a/examples/flask_me_example/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask_me_example.models import user -from social.apps.flask_app.me import models diff --git a/examples/flask_me_example/models/user.py b/examples/flask_me_example/models/user.py deleted file mode 100644 index d351fed77..000000000 --- a/examples/flask_me_example/models/user.py +++ /dev/null @@ -1,16 +0,0 @@ -from mongoengine import StringField, EmailField, BooleanField - -from flask_login import UserMixin - -from flask_me_example import db - - -class User(db.Document, UserMixin): - username = StringField(max_length=200) - password = StringField(max_length=200, default='') - name = StringField(max_length=100) - email = EmailField() - active = BooleanField(default=True) - - def is_active(self): - return self.active diff --git a/examples/flask_me_example/requirements.txt b/examples/flask_me_example/requirements.txt deleted file mode 100644 index e79691a4a..000000000 --- a/examples/flask_me_example/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -Flask -Flask-Login -Flask-Script -Werkzeug -Jinja2 -mongoengine==0.8.4 -python-social-auth diff --git a/examples/flask_me_example/routes/__init__.py b/examples/flask_me_example/routes/__init__.py deleted file mode 100644 index 0d41bfd26..000000000 --- a/examples/flask_me_example/routes/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask_me_example.routes import main -from social.apps.flask_app import routes diff --git a/examples/flask_me_example/routes/main.py b/examples/flask_me_example/routes/main.py deleted file mode 100644 index d2cfd82d5..000000000 --- a/examples/flask_me_example/routes/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import render_template, redirect -from flask_login import login_required, logout_user - -from flask_me_example import app - - -@app.route('/') -def main(): - return render_template('home.html') - - -@app.route('/done/') -@login_required -def done(): - return render_template('done.html') - - -@app.route('/logout') -def logout(): - """Logout view""" - logout_user() - return redirect('/') diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py deleted file mode 100644 index cf4ed21e9..000000000 --- a/examples/flask_me_example/settings.py +++ /dev/null @@ -1,62 +0,0 @@ -from flask_me_example import app - - -app.debug = True - -SECRET_KEY = 'random-secret-key' -SESSION_COOKIE_NAME = 'psa_session' -DEBUG = False - -MONGODB_SETTINGS = {'DB': 'psa_db'} - -DEBUG_TB_INTERCEPT_REDIRECTS = False -SESSION_PROTECTION = 'strong' - -SOCIAL_AUTH_LOGIN_URL = '/' -SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/' -SOCIAL_AUTH_USER_MODEL = 'flask_me_example.models.user.User' -SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.lastfm.LastFmAuth', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', -) diff --git a/examples/flask_me_example/templates/base.html b/examples/flask_me_example/templates/base.html deleted file mode 100644 index 86db50440..000000000 --- a/examples/flask_me_example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/flask_me_example/templates/done.html b/examples/flask_me_example/templates/done.html deleted file mode 100644 index ccabf53ec..000000000 --- a/examples/flask_me_example/templates/done.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

          You are logged in as {{ user.username }}!

          - -

          Associated:

          -{% for assoc in backends.associated %} -
          - {{ assoc.provider }} -
          - -
          -
          -{% endfor %} - -

          Associate:

          -
            - {% for name in backends.not_associated %} -
          • - {{ name }} -
          • - {% endfor %} -
          -{% endblock %} diff --git a/examples/flask_me_example/templates/home.html b/examples/flask_me_example/templates/home.html deleted file mode 100644 index c5c2a3ed7..000000000 --- a/examples/flask_me_example/templates/home.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -LastFm
          -ThisIsMyJam OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Clef OAuth2
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Podio OAuth2
          - -
          -
          - - - -
          -
          - -
          -
          - - - -
          -
          - -
          - - Persona -
          -{% endblock %} - -{% block scripts %} - - - -{% endblock %} diff --git a/examples/flask_peewee_example/__init__.py b/examples/flask_peewee_example/__init__.py deleted file mode 100644 index 4c2543f26..000000000 --- a/examples/flask_peewee_example/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -import sys - -from flask import Flask, g -from flask.ext import login - -sys.path.append('../..') - -from social.apps.flask_app.routes import social_auth -from social.apps.flask_app.template_filters import backends -from social.apps.flask_app.peewee.models import * -from peewee import * - -# App -app = Flask(__name__) -app.config.from_object('flask_example.settings') - -try: - app.config.from_object('flask_example.local_settings') -except ImportError: - pass - -from models.user import database_proxy, User - -# DB -database = SqliteDatabase('test.db') -database_proxy.initialize(database) - -app.register_blueprint(social_auth) -init_social(app, database) - -login_manager = login.LoginManager() -login_manager.login_view = 'main' -login_manager.login_message = '' -login_manager.init_app(app) - -from flask_example import models -from flask_example import routes - - -@login_manager.user_loader -def load_user(userid): - try: - us = User.get(User.id == userid) - return us - except User.DoesNotExist: - pass - - -@app.before_request -def global_user(): - g.user = login.current_user._get_current_object() - - -@app.context_processor -def inject_user(): - try: - return {'user': g.user} - except AttributeError: - return {'user': None} - - -app.context_processor(backends) diff --git a/examples/flask_peewee_example/manage.py b/examples/flask_peewee_example/manage.py deleted file mode 100644 index 927058674..000000000 --- a/examples/flask_peewee_example/manage.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -import sys - -from flask.ext.script import Server, Manager, Shell - -sys.path.append('..') - -from flask_example import app, database - - -manager = Manager(app) -manager.add_command('runserver', Server()) -manager.add_command('shell', Shell(make_context=lambda: { - 'app': app -})) - - -@manager.command -def syncdb(): - from flask_example.models.user import User - from social.apps.flask_app.peewee.models import FlaskStorage - - database.create_tables([User, FlaskStorage.user, FlaskStorage.nonce, FlaskStorage.association, FlaskStorage.code]) - -if __name__ == '__main__': - manager.run() diff --git a/examples/flask_peewee_example/models/__init__.py b/examples/flask_peewee_example/models/__init__.py deleted file mode 100644 index 2253824a3..000000000 --- a/examples/flask_peewee_example/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from flask_example.models import user -from social.apps.flask_app.peewee import models -# create a peewee database instance -- our models will use this database to -# persist information diff --git a/examples/flask_peewee_example/models/user.py b/examples/flask_peewee_example/models/user.py deleted file mode 100644 index a46f559cb..000000000 --- a/examples/flask_peewee_example/models/user.py +++ /dev/null @@ -1,24 +0,0 @@ -from peewee import * -from datetime import datetime -from flask.ext.login import UserMixin - -database_proxy = Proxy() - - -# model definitions -- the standard "pattern" is to define a base model class -# that specifies which database to use. then, any subclasses will automatically -# use the correct storage. -class BaseModel(Model): - class Meta: - database = database_proxy - -# the user model specifies its fields (or columns) declaratively, like django -class User(BaseModel, UserMixin): - username = CharField(unique=True) - password = CharField(null=True) - email = CharField(null=True) - active = BooleanField(default=True) - join_date = DateTimeField(default=datetime.now) - - class Meta: - order_by = ('username',) diff --git a/examples/flask_peewee_example/requirements.txt b/examples/flask_peewee_example/requirements.txt deleted file mode 100644 index e52656b9e..000000000 --- a/examples/flask_peewee_example/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -Peewee -Flask -Flask-Login -Flask-Script -Werkzeug -pysqlite -Jinja2 diff --git a/examples/flask_peewee_example/routes/__init__.py b/examples/flask_peewee_example/routes/__init__.py deleted file mode 100644 index d3586e81b..000000000 --- a/examples/flask_peewee_example/routes/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask_example.routes import main -from social.apps.flask_app import routes diff --git a/examples/flask_peewee_example/routes/main.py b/examples/flask_peewee_example/routes/main.py deleted file mode 100644 index 5e0dd9cbc..000000000 --- a/examples/flask_peewee_example/routes/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import render_template, redirect -from flask.ext.login import login_required, logout_user - -from flask_example import app - - -@app.route('/') -def main(): - return render_template('home.html') - - -@login_required -@app.route('/done/') -def done(): - return render_template('done.html') - - -@app.route('/logout') -def logout(): - """Logout view""" - logout_user() - return redirect('/') diff --git a/examples/flask_peewee_example/settings.py b/examples/flask_peewee_example/settings.py deleted file mode 100644 index 419c57518..000000000 --- a/examples/flask_peewee_example/settings.py +++ /dev/null @@ -1,55 +0,0 @@ -from os.path import dirname, abspath - -SECRET_KEY = 'random-secret-key' -SESSION_COOKIE_NAME = 'psa_session' -DEBUG = True -DEBUG_TB_INTERCEPT_REDIRECTS = False -SESSION_PROTECTION = 'strong' - -SOCIAL_AUTH_STORAGE = 'social.apps.flask_app.peewee.models.FlaskStorage' -SOCIAL_AUTH_LOGIN_URL = '/' -SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/' -SOCIAL_AUTH_USER_MODEL = 'flask_example.models.user.User' -SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', -) diff --git a/examples/flask_peewee_example/templates/base.html b/examples/flask_peewee_example/templates/base.html deleted file mode 100644 index 86db50440..000000000 --- a/examples/flask_peewee_example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/flask_peewee_example/templates/done.html b/examples/flask_peewee_example/templates/done.html deleted file mode 100644 index ccabf53ec..000000000 --- a/examples/flask_peewee_example/templates/done.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

          You are logged in as {{ user.username }}!

          - -

          Associated:

          -{% for assoc in backends.associated %} -
          - {{ assoc.provider }} -
          - -
          -
          -{% endfor %} - -

          Associate:

          -
            - {% for name in backends.not_associated %} -
          • - {{ name }} -
          • - {% endfor %} -
          -{% endblock %} diff --git a/examples/flask_peewee_example/templates/home.html b/examples/flask_peewee_example/templates/home.html deleted file mode 100644 index 1c7f9bcef..000000000 --- a/examples/flask_peewee_example/templates/home.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -ThisIsMyJam OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Clef OAuth2
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Podio OAuth2
          -MineID OAuth2
          - -
          -
          - - - -
          -
          - -
          -
          - - - -
          -
          - -
          - - Persona -
          -{% endblock %} - -{% block scripts %} - - - -{% endblock %} diff --git a/examples/pyramid_example/CHANGES.txt b/examples/pyramid_example/CHANGES.txt deleted file mode 100644 index 35a34f332..000000000 --- a/examples/pyramid_example/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version diff --git a/examples/pyramid_example/MANIFEST.in b/examples/pyramid_example/MANIFEST.in deleted file mode 100644 index dba331c6c..000000000 --- a/examples/pyramid_example/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include *.txt *.ini *.cfg *.rst -recursive-include example *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/examples/pyramid_example/README.txt b/examples/pyramid_example/README.txt deleted file mode 100644 index 0f2dc4705..000000000 --- a/examples/pyramid_example/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -example README -============== - -Getting Started ---------------- - -- cd - -- $VENV/bin/python setup.py develop - -- $VENV/bin/initialize_example_db development.ini - -- $VENV/bin/pserve development.ini - diff --git a/examples/pyramid_example/development.ini b/examples/pyramid_example/development.ini deleted file mode 100644 index 8b2cc169f..000000000 --- a/examples/pyramid_example/development.ini +++ /dev/null @@ -1,71 +0,0 @@ -### -# app configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html -### - -[app:main] -use = egg:example - -pyramid.reload_templates = true -pyramid.debug_authorization = false -pyramid.debug_notfound = true -pyramid.debug_routematch = true -pyramid.default_locale_name = en -pyramid.includes = - pyramid_debugtoolbar - pyramid_tm - -sqlalchemy.url = sqlite:///%(here)s/test.db - -# By default, the toolbar only appears for clients from IP addresses -# '127.0.0.1' and '::1'. -# debugtoolbar.hosts = 127.0.0.1 ::1 - -### -# wsgi server configuration -### - -[server:main] -use = egg:waitress#main -host = 0.0.0.0 -port = 8000 - -### -# logging configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html -### - -[loggers] -keys = root, example, sqlalchemy - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = INFO -handlers = console - -[logger_example] -level = DEBUG -handlers = -qualname = example - -[logger_sqlalchemy] -level = INFO -handlers = -qualname = sqlalchemy.engine -# "level = INFO" logs SQL queries. -# "level = DEBUG" logs SQL queries and results. -# "level = WARN" logs neither. (Recommended for production systems.) - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/examples/pyramid_example/example/__init__.py b/examples/pyramid_example/example/__init__.py deleted file mode 100644 index bc0e6b7e8..000000000 --- a/examples/pyramid_example/example/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys - -sys.path.append('../..') - -from pyramid.config import Configurator -from pyramid.session import UnencryptedCookieSessionFactoryConfig -from sqlalchemy import engine_from_config - -from social.apps.pyramid_app.models import init_social - -from .models import DBSession, Base - - -def main(global_config, **settings): - """This function returns a Pyramid WSGI application.""" - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine - session_factory = UnencryptedCookieSessionFactoryConfig('thisisasecret') - config = Configurator(settings=settings, - session_factory=session_factory, - autocommit=True) - config.include('pyramid_chameleon') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_request_method('example.auth.get_user', 'user', reify=True) - config.add_route('home', '/') - config.add_route('done', '/done') - config.include('example.settings') - config.include('example.local_settings') - config.include('social.apps.pyramid_app') - init_social(config, Base, DBSession) - config.scan() - config.scan('social.apps.pyramid_app') - return config.make_wsgi_app() diff --git a/examples/pyramid_example/example/auth.py b/examples/pyramid_example/example/auth.py deleted file mode 100644 index 23926fe5e..000000000 --- a/examples/pyramid_example/example/auth.py +++ /dev/null @@ -1,30 +0,0 @@ -from pyramid.events import subscriber, BeforeRender - -from social.apps.pyramid_app.utils import backends - -from example.models import DBSession, User - - -def login_user(backend, user, user_social_auth): - backend.strategy.session_set('user_id', user.id) - - -def login_required(request): - return getattr(request, 'user', None) is not None - - -def get_user(request): - user_id = request.session.get('user_id') - if user_id: - user = DBSession.query(User)\ - .filter(User.id == user_id)\ - .first() - else: - user = None - return user - - -@subscriber(BeforeRender) -def add_social(event): - request = event['request'] - event['social'] = backends(request, request.user) diff --git a/examples/pyramid_example/example/local_settings.py.template b/examples/pyramid_example/example/local_settings.py.template deleted file mode 100644 index 8fac8433a..000000000 --- a/examples/pyramid_example/example/local_settings.py.template +++ /dev/null @@ -1,95 +0,0 @@ -SOCIAL_AUTH_KEYS = { - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': '', - 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': '', - - 'SOCIAL_AUTH_TWITTER_KEY': '', - 'SOCIAL_AUTH_TWITTER_SECRET': '', - - 'SOCIAL_AUTH_STRIPE_KEY': '', - 'SOCIAL_AUTH_STRIPE_SECRET': '', - 'SOCIAL_AUTH_STRIPE_SCOPE': [], - - 'SOCIAL_AUTH_FACEBOOK_KEY': '', - 'SOCIAL_AUTH_FACEBOOK_SECRET': '', - - 'SOCIAL_AUTH_FACEBOOK_APP_KEY': '', - 'SOCIAL_AUTH_FACEBOOK_APP_SECRET': '', - 'SOCIAL_AUTH_FACEBOOK_APP_NAMESPACE': '', - - 'SOCIAL_AUTH_YAHOO_OAUTH_KEY': '', - 'SOCIAL_AUTH_YAHOO_OAUTH_SECRET': '', - - 'SOCIAL_AUTH_ANGEL_KEY': '', - 'SOCIAL_AUTH_ANGEL_SECRET': '', - - 'SOCIAL_AUTH_BEHANCE_KEY': '', - 'SOCIAL_AUTH_BEHANCE_SECRET': '', - 'SOCIAL_AUTH_BEHANCE_SCOPE': [], - - 'SOCIAL_AUTH_BITBUCKET_KEY': '', - 'SOCIAL_AUTH_BITBUCKET_SECRET': '', - - 'SOCIAL_AUTH_LINKEDIN_KEY': '', - 'SOCIAL_AUTH_LINKEDIN_SECRET': '', - 'SOCIAL_AUTH_LINKEDIN_SCOPE': [], - - 'SOCIAL_AUTH_GITHUB_KEY': '', - 'SOCIAL_AUTH_GITHUB_SECRET': '', - - 'SOCIAL_AUTH_FOURSQUARE_KEY': '', - 'SOCIAL_AUTH_FOURSQUARE_SECRET': '', - - 'SOCIAL_AUTH_INSTAGRAM_KEY': '', - 'SOCIAL_AUTH_INSTAGRAM_SECRET': '', - - 'SOCIAL_AUTH_LIVE_KEY': '', - 'SOCIAL_AUTH_LIVE_SECRET': '', - - 'SOCIAL_AUTH_VKONTAKTE_OAUTH2_KEY': '', - 'SOCIAL_AUTH_VKONTAKTE_OAUTH2_SECRET': '', - - 'SOCIAL_AUTH_DAILYMOTION_KEY': '', - 'SOCIAL_AUTH_DAILYMOTION_SECRET': '', - - 'SOCIAL_AUTH_DISQUS_KEY': '', - 'SOCIAL_AUTH_DISQUS_SECRET': '', - - 'SOCIAL_AUTH_DROPBOX_KEY': '', - 'SOCIAL_AUTH_DROPBOX_SECRET': '', - - 'SOCIAL_AUTH_EVERNOTE_SANDBOX_KEY': '', - 'SOCIAL_AUTH_EVERNOTE_SANDBOX_SECRET': '', - - 'SOCIAL_AUTH_FITBIT_KEY': '', - 'SOCIAL_AUTH_FITBIT_SECRET': '', - - 'SOCIAL_AUTH_FLICKR_KEY': '', - 'SOCIAL_AUTH_FLICKR_SECRET': '', - - 'SOCIAL_AUTH_SOUNDCLOUD_KEY': '', - 'SOCIAL_AUTH_SOUNDCLOUD_SECRET': '', - - 'SOCIAL_AUTH_STOCKTWITS_KEY': '', - 'SOCIAL_AUTH_STOCKTWITS_SECRET': '', - - 'SOCIAL_AUTH_TRIPIT_KEY': '', - 'SOCIAL_AUTH_TRIPIT_SECRET': '', - - 'SOCIAL_AUTH_TWILIO_KEY': '', - 'SOCIAL_AUTH_TWILIO_SECRET': '', - - 'SOCIAL_AUTH_XING_KEY': '', - 'SOCIAL_AUTH_XING_SECRET': '', - - 'SOCIAL_AUTH_YANDEX_OAUTH2_KEY': '', - 'SOCIAL_AUTH_YANDEX_OAUTH2_SECRET': '', - 'SOCIAL_AUTH_YANDEX_OAUTH2_API_URL': '', - - 'SOCIAL_AUTH_REDDIT_KEY': '', - 'SOCIAL_AUTH_REDDIT_SECRET': '', - 'SOCIAL_AUTH_REDDIT_AUTH_EXTRA_ARGUMENTS': {}, -} - - -def includeme(config): - config.registry.settings.update(SOCIAL_AUTH_KEYS) diff --git a/examples/pyramid_example/example/models.py b/examples/pyramid_example/example/models.py deleted file mode 100644 index 03b609886..000000000 --- a/examples/pyramid_example/example/models.py +++ /dev/null @@ -1,19 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session, sessionmaker - -from zope.sqlalchemy import ZopeTransactionExtension - - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class User(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - username = Column(String(200)) - email = Column(String(200)) - password = Column(String(200), default='') - name = Column(String(100)) - active = Column(Boolean, default=True) diff --git a/examples/pyramid_example/example/scripts/__init__.py b/examples/pyramid_example/example/scripts/__init__.py deleted file mode 100644 index 5bb534f79..000000000 --- a/examples/pyramid_example/example/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# package diff --git a/examples/pyramid_example/example/scripts/initializedb.py b/examples/pyramid_example/example/scripts/initializedb.py deleted file mode 100644 index c3ced8830..000000000 --- a/examples/pyramid_example/example/scripts/initializedb.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import sys - -sys.path.append('../..') - -from sqlalchemy import engine_from_config - -from pyramid.paster import get_appsettings, setup_logging -from pyramid.scripts.common import parse_vars - -from social.apps.pyramid_app.models import init_social - -from example.models import DBSession, Base -from example.settings import SOCIAL_AUTH_SETTINGS - - -def usage(argv): - cmd = os.path.basename(argv[0]) - print('usage: %s [var=value]\n' - '(example: "%s development.ini")' % (cmd, cmd)) - sys.exit(1) - - -def main(argv=sys.argv): - if len(argv) < 2: - usage(argv) - config_uri = argv[1] - options = parse_vars(argv[2:]) - setup_logging(config_uri) - settings = get_appsettings(config_uri, options=options) - init_social(SOCIAL_AUTH_SETTINGS, Base, DBSession) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.create_all(engine) - - -if __name__ == '__main__': - main() diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py deleted file mode 100644 index 05e2d9eef..000000000 --- a/examples/pyramid_example/example/settings.py +++ /dev/null @@ -1,55 +0,0 @@ -SOCIAL_AUTH_SETTINGS = { - 'SOCIAL_AUTH_LOGIN_URL': '/', - 'SOCIAL_AUTH_LOGIN_REDIRECT_URL': '/done', - 'SOCIAL_AUTH_USER_MODEL': 'example.models.User', - 'SOCIAL_AUTH_LOGIN_FUNCTION': 'example.auth.login_user', - 'SOCIAL_AUTH_LOGGEDIN_FUNCTION': 'example.auth.login_required', - 'SOCIAL_AUTH_AUTHENTICATION_BACKENDS': ( - 'social.backends.twitter.TwitterOAuth', - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.twilio.TwilioAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', - ) -} - - -def includeme(config): - config.registry.settings.update(SOCIAL_AUTH_SETTINGS) diff --git a/examples/pyramid_example/example/templates/done.pt b/examples/pyramid_example/example/templates/done.pt deleted file mode 100644 index d073f26ba..000000000 --- a/examples/pyramid_example/example/templates/done.pt +++ /dev/null @@ -1,24 +0,0 @@ - - - - Social Auth Pyramid Example - - - -

          You are logged in as ${request.user.username}!

          -

          Associated:

          -
          -
          - - -
          -
          - -

          Associate:

          - - - diff --git a/examples/pyramid_example/example/templates/home.pt b/examples/pyramid_example/example/templates/home.pt deleted file mode 100644 index cc6494160..000000000 --- a/examples/pyramid_example/example/templates/home.pt +++ /dev/null @@ -1,91 +0,0 @@ - - - - Social Auth Pyramid Example - - - - Google OAuth2
          - Google OAuth
          - Google OpenId
          - Twitter OAuth
          - Yahoo OpenId
          - Yahoo OAuth
          - Stripe OAuth2
          - Facebook OAuth2
          - Facebook App
          - Angel OAuth2
          - Behance OAuth2
          - Bitbucket OAuth
          - Box OAuth2
          - LinkedIn OAuth
          - Github OAuth2
          - Foursquare OAuth2
          - Instagram OAuth2
          - Live OAuth2
          - VK.com OAuth2
          - Dailymotion OAuth2
          - Disqus OAuth2
          - Dropbox OAuth
          - Evernote OAuth (sandbox mode)
          - Fitbit OAuth
          - Flickr OAuth
          - Soundcloud OAuth2
          - ThisIsMyJam OAuth1
          - Stocktwits OAuth2
          - Tripit OAuth
          - Clef OAuth2
          - Twilio
          - Xing OAuth
          - Yandex OAuth2
          - Podio OAuth2
          - Reddit OAuth2
          - -
          - -
          - - - -
          -
          - -
          - -
          - - - -
          -
          - -
          - - - Persona -
          - - - - - - diff --git a/examples/pyramid_example/example/tests.py b/examples/pyramid_example/example/tests.py deleted file mode 100644 index 219a2d4a9..000000000 --- a/examples/pyramid_example/example/tests.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -import transaction - -from pyramid import testing - -from .models import DBSession - - -class TestMyViewSuccessCondition(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, - ) - DBSession.configure(bind=engine) - Base.metadata.create_all(engine) - with transaction.manager: - model = MyModel(name='one', value=55) - DBSession.add(model) - - def tearDown(self): - DBSession.remove() - testing.tearDown() - - def test_passing_view(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['one'].name, 'one') - self.assertEqual(info['project'], 'example') - - -class TestMyViewFailureCondition(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, - ) - DBSession.configure(bind=engine) - - def tearDown(self): - DBSession.remove() - testing.tearDown() - - def test_failing_view(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info.status_int, 500) diff --git a/examples/pyramid_example/example/views.py b/examples/pyramid_example/example/views.py deleted file mode 100644 index dc583982c..000000000 --- a/examples/pyramid_example/example/views.py +++ /dev/null @@ -1,11 +0,0 @@ -from pyramid.view import view_config - - -@view_config(route_name='home', renderer='templates/home.pt') -def home(request): - return {} - - -@view_config(route_name='done', renderer='templates/done.pt') -def done(request): - return {} diff --git a/examples/pyramid_example/production.ini b/examples/pyramid_example/production.ini deleted file mode 100644 index 315ef45a0..000000000 --- a/examples/pyramid_example/production.ini +++ /dev/null @@ -1,62 +0,0 @@ -### -# app configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html -### - -[app:main] -use = egg:example - -pyramid.reload_templates = false -pyramid.debug_authorization = false -pyramid.debug_notfound = false -pyramid.debug_routematch = false -pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm - -sqlalchemy.url = sqlite:///%(here)s/test.db - -[server:main] -use = egg:waitress#main -host = 0.0.0.0 -port = 6543 - -### -# logging configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html -### - -[loggers] -keys = root, example, sqlalchemy - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console - -[logger_example] -level = WARN -handlers = -qualname = example - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine -# "level = INFO" logs SQL queries. -# "level = DEBUG" logs SQL queries and results. -# "level = WARN" logs neither. (Recommended for production systems.) - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/examples/pyramid_example/requirements.txt b/examples/pyramid_example/requirements.txt deleted file mode 100644 index 4a815a9ea..000000000 --- a/examples/pyramid_example/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -python-social-auth -pyramid diff --git a/examples/pyramid_example/setup.cfg b/examples/pyramid_example/setup.cfg deleted file mode 100644 index 26cfa06be..000000000 --- a/examples/pyramid_example/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=example -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = example/locale -domain = example -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = example/locale/example.pot -width = 80 - -[init_catalog] -domain = example -input_file = example/locale/example.pot -output_dir = example/locale - -[update_catalog] -domain = example -input_file = example/locale/example.pot -output_dir = example/locale -previous = true diff --git a/examples/pyramid_example/setup.py b/examples/pyramid_example/setup.py deleted file mode 100644 index 0b4318e67..000000000 --- a/examples/pyramid_example/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'pyramid', - 'SQLAlchemy', - 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', - 'zope.sqlalchemy', - 'waitress', - 'pyramid_chameleon', - ] - -setup(name='example', - version='0.0', - description='example', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - "Programming Language :: Python", - "Framework :: Pyramid", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], - author='', - author_email='', - url='', - keywords='web wsgi bfg pylons pyramid', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - test_suite='example', - install_requires=requires, - entry_points="""\ - [paste.app_factory] - main = example:main - [console_scripts] - initialize_example_db = example.scripts.initializedb:main - """, - ) diff --git a/examples/tornado_example/__init__.py b/examples/tornado_example/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/tornado_example/app.py b/examples/tornado_example/app.py deleted file mode 100644 index ad900ed98..000000000 --- a/examples/tornado_example/app.py +++ /dev/null @@ -1,71 +0,0 @@ -import sys - -sys.path.append('../..') - -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session, sessionmaker - -import tornado.httpserver -import tornado.ioloop -import tornado.options -import tornado.web - -from social.apps.tornado_app.models import init_social -from social.apps.tornado_app.routes import SOCIAL_AUTH_ROUTES - -import settings - - -engine = create_engine('sqlite:///test.db', echo=False) -session = scoped_session(sessionmaker(bind=engine)) -Base = declarative_base() - - -class MainHandler(tornado.web.RequestHandler): - def get(self): - self.render('templates/home.html') - - -class DoneHandler(tornado.web.RequestHandler): - def get(self, *args, **kwargs): - from models import User - user_id = self.get_secure_cookie('user_id') - user = session.query(User).get(int(user_id)) - self.render('templates/done.html', user=user) - - -class LogoutHandler(tornado.web.RequestHandler): - def get(self): - self.request.redirect('/') - - -tornado.options.parse_command_line() -tornado_settings = dict((k, getattr(settings, k)) for k in dir(settings) - if not k.startswith('__')) -application = tornado.web.Application(SOCIAL_AUTH_ROUTES + [ - (r'/', MainHandler), - (r'/done/', DoneHandler), - (r'/logout/', LogoutHandler), -], cookie_secret='adb528da-20bb-4386-8eaf-09f041b569e0', - **tornado_settings) - - -def main(): - init_social(Base, session, tornado_settings) - http_server = tornado.httpserver.HTTPServer(application) - http_server.listen(8000) - tornado.ioloop.IOLoop.instance().start() - - -def syncdb(): - from models import user_syncdb - init_social(Base, session, tornado_settings) - Base.metadata.create_all(engine) - user_syncdb() - -if __name__ == '__main__': - if len(sys.argv) > 1 and sys.argv[1] == 'syncdb': - syncdb() - else: - main() diff --git a/examples/tornado_example/models.py b/examples/tornado_example/models.py deleted file mode 100644 index 19b98a972..000000000 --- a/examples/tornado_example/models.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import Column, Integer, String - -from app import Base, engine - - -class User(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - username = Column(String(30), nullable=False) - first_name = Column(String(30), nullable=True) - last_name = Column(String(30), nullable=True) - email = Column(String(75), nullable=False) - password = Column(String(128), nullable=True) - - -def user_syncdb(): - Base.metadata.create_all(engine) diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py deleted file mode 100644 index 81ae2eb5d..000000000 --- a/examples/tornado_example/settings.py +++ /dev/null @@ -1,51 +0,0 @@ -SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db' - -SOCIAL_AUTH_LOGIN_URL = '/' -SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/' -SOCIAL_AUTH_USER_MODEL = 'models.User' -SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.reddit.RedditOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', -) - -from local_settings import * diff --git a/examples/tornado_example/templates/base.html b/examples/tornado_example/templates/base.html deleted file mode 100644 index a4e3df313..000000000 --- a/examples/tornado_example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% end %} - {% block scripts %}{% end %} - - - - - diff --git a/examples/tornado_example/templates/done.html b/examples/tornado_example/templates/done.html deleted file mode 100644 index 2684a531d..000000000 --- a/examples/tornado_example/templates/done.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

          You are logged in as {{ user.username }}!

          -{% end %} diff --git a/examples/tornado_example/templates/home.html b/examples/tornado_example/templates/home.html deleted file mode 100644 index 8c8c766d9..000000000 --- a/examples/tornado_example/templates/home.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -ThisIsMyJam OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Clef OAuth2
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Podio OAuth2
          -MineID OAuth2
          - -
          -
          - - - -
          -
          - -
          -
          - - - -
          -
          - -
          - - Persona -
          -{% end %} - -{% block scripts %} - - - -{% end %} diff --git a/examples/webpy_example/__init__.py b/examples/webpy_example/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py deleted file mode 100644 index 2224d93ec..000000000 --- a/examples/webpy_example/app.py +++ /dev/null @@ -1,116 +0,0 @@ -import sys - -sys.path.append('../..') - -import web -from web.contrib.template import render_jinja - -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker - -from social.utils import setting_name -from social.apps.webpy_app.utils import psa, backends -from social.apps.webpy_app import app as social_app - -import local_settings - -web.config.debug = False -web.config[setting_name('USER_MODEL')] = 'models.User' -web.config[setting_name('AUTHENTICATION_BACKENDS')] = ( - 'social.backends.open_id.OpenIdAuth', - 'social.backends.google.GoogleOpenId', - 'social.backends.google.GoogleOAuth2', - 'social.backends.google.GoogleOAuth', - 'social.backends.twitter.TwitterOAuth', - 'social.backends.yahoo.YahooOpenId', - 'social.backends.stripe.StripeOAuth2', - 'social.backends.persona.PersonaAuth', - 'social.backends.facebook.FacebookOAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.yahoo.YahooOAuth', - 'social.backends.angel.AngelOAuth2', - 'social.backends.behance.BehanceOAuth2', - 'social.backends.bitbucket.BitbucketOAuth', - 'social.backends.box.BoxOAuth2', - 'social.backends.linkedin.LinkedinOAuth', - 'social.backends.github.GithubOAuth2', - 'social.backends.foursquare.FoursquareOAuth2', - 'social.backends.instagram.InstagramOAuth2', - 'social.backends.live.LiveOAuth2', - 'social.backends.vk.VKOAuth2', - 'social.backends.dailymotion.DailymotionOAuth2', - 'social.backends.disqus.DisqusOAuth2', - 'social.backends.dropbox.DropboxOAuth', - 'social.backends.eveonline.EVEOnlineOAuth2', - 'social.backends.evernote.EvernoteSandboxOAuth', - 'social.backends.fitbit.FitbitOAuth2', - 'social.backends.flickr.FlickrOAuth', - 'social.backends.livejournal.LiveJournalOpenId', - 'social.backends.soundcloud.SoundcloudOAuth2', - 'social.backends.thisismyjam.ThisIsMyJamOAuth1', - 'social.backends.stocktwits.StocktwitsOAuth2', - 'social.backends.tripit.TripItOAuth', - 'social.backends.clef.ClefOAuth2', - 'social.backends.twilio.TwilioAuth', - 'social.backends.xing.XingOAuth', - 'social.backends.yandex.YandexOAuth2', - 'social.backends.podio.PodioOAuth2', - 'social.backends.mineid.MineIDOAuth2', - 'social.backends.wunderlist.WunderlistOAuth2', - 'social.backends.upwork.UpworkOAuth', -) -web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/' - - -urls = ( - '^/$', 'main', - '^/done/$', 'done', - '', social_app.app_social -) - - -render = render_jinja('templates/') - - -class main(object): - def GET(self): - return render.home() - - -class done(social_app.BaseViewClass): - def GET(self): - user = self.get_current_user() - return render.done(user=user, backends=backends(user)) - - -engine = create_engine('sqlite:///test.db', echo=True) - - -def load_sqla(handler): - web.ctx.orm = scoped_session(sessionmaker(bind=engine)) - try: - return handler() - except web.HTTPError: - web.ctx.orm.commit() - raise - except: - web.ctx.orm.rollback() - raise - finally: - web.ctx.orm.commit() - # web.ctx.orm.expunge_all() - - -Session = sessionmaker(bind=engine) -Session.configure(bind=engine) - -app = web.application(urls, locals()) -app.add_processor(load_sqla) -session = web.session.Session(app, web.session.DiskStore('sessions')) - -web.db_session = Session() -web.web_session = session - - -if __name__ == "__main__": - app.run() diff --git a/examples/webpy_example/migrate.py b/examples/webpy_example/migrate.py deleted file mode 100644 index 154861143..000000000 --- a/examples/webpy_example/migrate.py +++ /dev/null @@ -1,8 +0,0 @@ -from app import engine -from models import Base -from social.apps.webpy_app.models import SocialBase - - -if __name__ == '__main__': - Base.metadata.create_all(engine) - SocialBase.metadata.create_all(engine) diff --git a/examples/webpy_example/models.py b/examples/webpy_example/models.py deleted file mode 100644 index f9cc7a790..000000000 --- a/examples/webpy_example/models.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean -from sqlalchemy.ext.declarative import declarative_base - - -Base = declarative_base() - - -class User(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - username = Column(String(200)) - password = Column(String(200), default='') - name = Column(String(100)) - email = Column(String(200)) - active = Column(Boolean, default=True) - - def is_active(self): - return self.active - - def is_authenticated(self): - return True diff --git a/examples/webpy_example/requirements.txt b/examples/webpy_example/requirements.txt deleted file mode 100644 index accc6fda4..000000000 --- a/examples/webpy_example/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Jinja2==2.6 -web.py==0.37 -python-social-auth diff --git a/examples/webpy_example/templates/base.html b/examples/webpy_example/templates/base.html deleted file mode 100644 index 86db50440..000000000 --- a/examples/webpy_example/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Social - - - - {% block content %}{% endblock %} - {% block scripts %}{% endblock %} - - - - - diff --git a/examples/webpy_example/templates/done.html b/examples/webpy_example/templates/done.html deleted file mode 100644 index 0ee48cff5..000000000 --- a/examples/webpy_example/templates/done.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Logged in as {{ user.username }}! - -

          Associated:

          - -{% for assoc in backends["associated"] %} -
          - {{ assoc.provider }} -
          - -
          -
          -{% endfor %} - -

          Associate:

          -
            - {% for name in backends["not_associated"] %} -
          • - {{ name }} -
          • - {% endfor %} -
          - -{% endblock %} diff --git a/examples/webpy_example/templates/home.html b/examples/webpy_example/templates/home.html deleted file mode 100644 index e83b5a4fc..000000000 --- a/examples/webpy_example/templates/home.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Google OAuth2
          -Google OAuth
          -Google OpenId
          -Twitter OAuth
          -Yahoo OpenId
          -Yahoo OAuth
          -Stripe OAuth2
          -Facebook OAuth2
          -Facebook App
          -Angel OAuth2
          -Behance OAuth2
          -Bitbucket OAuth
          -Box OAuth2
          -LinkedIn OAuth
          -Github OAuth2
          -Foursquare OAuth2
          -Instagram OAuth2
          -Live OAuth2
          -VK.com OAuth2
          -Dailymotion OAuth2
          -Disqus OAuth2
          -Dropbox OAuth
          -Evernote OAuth (sandbox mode)
          -Fitbit OAuth
          -Flickr OAuth
          -Soundcloud OAuth2
          -ThisIsMyJamm OAuth1
          -Stocktwits OAuth2
          -Tripit OAuth
          -Clef OAuth2
          -Twilio
          -Xing OAuth
          -Yandex OAuth2
          -Podio OAuth2
          -MineID OAuth2
          - -
          -
          - - - -
          -
          - -
          -
          - - - -
          -
          - -
          - - Persona -
          -{% endblock %} - -{% block scripts %} - - - -{% endblock %} From 5693f2f7ad80220f322e44af52afe167762bad44 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 5 Dec 2016 10:16:49 +0000 Subject: [PATCH 873/890] Update migration guide for Django --- MIGRATING_TO_SOCIAL.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/MIGRATING_TO_SOCIAL.md b/MIGRATING_TO_SOCIAL.md index baf23d489..2eb278f97 100644 --- a/MIGRATING_TO_SOCIAL.md +++ b/MIGRATING_TO_SOCIAL.md @@ -22,6 +22,19 @@ Django users need to add the `social-auth-app-django` dependency. Those using `mongoengine`, need to add `social-auth-app-mongoengine`. +### Settings + +- Update your references to `social.*` in your settings, most notably: + - `AUTHENTICATION_BACKENDS` are now under `social_core.*` + (e.g. `social_core.backends.facebook.FacebookOAuth2`). + - Context processors are now under `social_django` + (e.g. `social_django.context_processors.backends`). + - `MIDDLEWARE_CLASSES` are now under `social_django` + (e.g. `social_django.middleware.SocialAuthExceptionMiddleware`). + - If you have it overridden, `SOCIAL_AUTH_PIPELINE` setting. +- Update your `INSTALLED_APPS` to include `social_django` instead of +`social.apps.django_app.default`. + ## Flask Flask users need to add `social-auth-app-flask`, and depending on the From a12e2a2cf7b9d04efe9d041a6da769b613aaa1f0 Mon Sep 17 00:00:00 2001 From: Alasdair Nicol Date: Sat, 10 Dec 2016 11:50:52 +0000 Subject: [PATCH 874/890] Fixed typo in migrating to social docs --- MIGRATING_TO_SOCIAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATING_TO_SOCIAL.md b/MIGRATING_TO_SOCIAL.md index 2eb278f97..2b78e13da 100644 --- a/MIGRATING_TO_SOCIAL.md +++ b/MIGRATING_TO_SOCIAL.md @@ -1,6 +1,6 @@ # Migrating from python-social-auth to split social -Since Dec 03 2016, [python-socia-auth](https://github.com/omab/python-social-auth) +Since Dec 03 2016, [python-social-auth](https://github.com/omab/python-social-auth) is marked as deprecated and the community is recommended to migrate towards the packages created in the [organization repository](https://github.com/python-social-auth/social-core). From 30328d0a6fe4b57a94ce9534b263decb546401b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 26 Dec 2016 12:17:28 -0300 Subject: [PATCH 875/890] v0.3.1 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 0f1326029..98d9ecb06 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 3, 0) +version = (0, 3, 1) extra = '' __version__ = '.'.join(map(str, version)) + extra From 847b07b20d2903753fc7eb43b6ad8745b3e81bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 31 Dec 2016 11:32:49 -0300 Subject: [PATCH 876/890] Define extras to install frameworks integrations --- MIGRATING_TO_SOCIAL.md | 49 +++++++++++++++++++++++++++++++++++++++++- setup.py | 11 ++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/MIGRATING_TO_SOCIAL.md b/MIGRATING_TO_SOCIAL.md index 2b78e13da..948127fdd 100644 --- a/MIGRATING_TO_SOCIAL.md +++ b/MIGRATING_TO_SOCIAL.md @@ -14,7 +14,8 @@ the new libraries and defined a single dependency in the [requirements.txt](http file, `social-auth-core`, this aims to ease the transition to the new structure. But that won't solve everybody situation, people using the different -frameworks also need to define their corresponding requirement. +frameworks also need to define their corresponding requirement, or use +one of the defined `extras` in the `setup.py` file. ## Django @@ -35,6 +36,13 @@ dependency. Those using `mongoengine`, need to add - Update your `INSTALLED_APPS` to include `social_django` instead of `social.apps.django_app.default`. +### Extras supported + +``` +$ pip install python-social-auth[django] +$ pip install python-social-auth[django-mongoengine] +``` + ## Flask Flask users need to add `social-auth-app-flask`, and depending on the @@ -44,14 +52,53 @@ storage solution, add one of the following too: - `social-auth-app-flask-mongoengine` when using Mongoengine - `social-auth-app-flask-peewee` when using Peewee + +### Extras supported + +``` +$ pip install python-social-auth[flask] +$ pip install python-social-auth[flask-mongoengine] +$ pip install python-social-auth[flask-peewee] +``` + ## Pyramid Pyramid users need to add `social-auth-app-pyramid` to their dependencies. +### Extras supported + +``` +$ pip install python-social-auth[pyramid] +``` + ## Tornado Tornado users need to add `social-auth-app-tornado` to their dependencies. +### Extras supported + +``` +$ pip install python-social-auth[tornado] +``` + + ## Webpy Web.py users need to add `social-auth-app-webpy` to their dependencies. + +### Extras supported + +``` +$ pip install python-social-auth[webpy] +``` + + +## Cherrypy + +Cherrypy users need to add `social-auth-app-cherrypy` to their dependencies. + +### Extras supported + +``` +$ pip install python-social-auth[cherrypy] +``` diff --git a/setup.py b/setup.py index 41282fcad..1151cc9f8 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,17 @@ def get_packages(): package_data={ 'social/tests': ['social/tests/*.txt'] }, + extras_require={ + 'django': ['social-auth-app-django'], + 'django-mongoengine': ['social-auth-app-django-mongoengine'], + 'flask': ['social-auth-app-flask', 'social-auth-app-flask-sqlalchemy'], + 'flask-mongoengine': ['social-auth-app-flask-mongoengine'], + 'flask-peewee': ['social-auth-app-flask-peewee'], + 'cherrypy': ['social-auth-app-cherrypy'], + 'pyramid': ['social-auth-app-pyramid'], + 'tornado': ['social-auth-app-tornado'], + 'webpy': ['social-auth-app-webpy'] + }, include_package_data=True, tests_require=tests_requirements, test_suite='social.tests', From e6ca9a2ad9c4cfa6f67c7958940136107380d5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 31 Dec 2016 11:33:15 -0300 Subject: [PATCH 877/890] Version bump 0.3.1 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 98d9ecb06..5d9aa752a 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 3, 1) +version = (0, 3, 2) extra = '' __version__ = '.'.join(map(str, version)) + extra From 61c28b9d0ca81e416f77ca89fa502bcc48fcc2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 1 Jan 2017 08:32:20 -0300 Subject: [PATCH 878/890] Fix flask sqlalchemy imports --- social/apps/flask_app/default/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/apps/flask_app/default/models.py b/social/apps/flask_app/default/models.py index 1913b8951..0a0f89e35 100644 --- a/social/apps/flask_app/default/models.py +++ b/social/apps/flask_app/default/models.py @@ -1,2 +1,2 @@ -from social_flask.models import PSABase, _AppSession, UserSocialAuth, Nonce, \ +from social_flask_sqlalchemy.models import PSABase, _AppSession, UserSocialAuth, Nonce, \ Association, Code, FlaskStorage, init_social From 605957f79f9cd5f2272032b4191eca79a5834461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 1 Jan 2017 08:37:02 -0300 Subject: [PATCH 879/890] Version bump 0.3.3 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 5d9aa752a..c0376cb53 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 3, 2) +version = (0, 3, 3) extra = '' __version__ = '.'.join(map(str, version)) + extra From 5e95a32e30a711417201b5be3128556caab0d0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 1 Jan 2017 08:47:30 -0300 Subject: [PATCH 880/890] Fix get_pipeline() test method --- social/tests/strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/tests/strategy.py b/social/tests/strategy.py index d88685f2d..217c75145 100644 --- a/social/tests/strategy.py +++ b/social/tests/strategy.py @@ -105,7 +105,7 @@ def authenticate(self, *args, **kwargs): self.session_set('username', user.username) return user - def get_pipeline(self): + def get_pipeline(self, backend=None): return self.setting('PIPELINE', ( 'social.pipeline.social_auth.social_details', 'social.pipeline.social_auth.social_uid', From 692f3aa293698cfdc38ca6bd40dcdb35a8b526ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sun, 1 Jan 2017 10:57:00 -0300 Subject: [PATCH 881/890] Get rid of set/get current strategy --- social/strategies/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social/strategies/utils.py b/social/strategies/utils.py index 5c23071bb..d8b1dfc87 100644 --- a/social/strategies/utils.py +++ b/social/strategies/utils.py @@ -1,2 +1 @@ -from social_core.utils import get_strategy, set_current_strategy_getter, \ - get_current_strategy +from social_core.utils import get_strategy From 22d854dd8996bbbde4e4da835bc25388e75e9fb6 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 4 Jan 2017 16:25:11 +0000 Subject: [PATCH 882/890] Add url patterns to migration guide --- MIGRATING_TO_SOCIAL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MIGRATING_TO_SOCIAL.md b/MIGRATING_TO_SOCIAL.md index 948127fdd..265399c64 100644 --- a/MIGRATING_TO_SOCIAL.md +++ b/MIGRATING_TO_SOCIAL.md @@ -35,6 +35,8 @@ dependency. Those using `mongoengine`, need to add - If you have it overridden, `SOCIAL_AUTH_PIPELINE` setting. - Update your `INSTALLED_APPS` to include `social_django` instead of `social.apps.django_app.default`. +- Update your urls patterns to include `'social_django.urls'` instead of + `'social.apps.django_app.urls'`. ### Extras supported From b3b6d83b82f3c00ab1dddc9eaf56eaf72cadb202 Mon Sep 17 00:00:00 2001 From: Apocalepse Date: Sat, 7 Jan 2017 15:15:32 +0500 Subject: [PATCH 883/890] in social_core.backends.facebook only classes FacebookOAuth --- social/backends/facebook.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 611e5d566..f752af8d1 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -1,2 +1 @@ -from social_core.backends.facebook import FacebookOAuth2, FacebookAppOAuth2, \ - Facebook2OAuth2, Facebook2AppOAuth2 +from social_core.backends.facebook import FacebookOAuth2, FacebookAppOAuth2 From 680849f68d28ecda6cbab246dbb37a0aec1e001f Mon Sep 17 00:00:00 2001 From: Raphael Das Gupta Date: Fri, 13 Jan 2017 11:26:44 +0100 Subject: [PATCH 884/890] fix date in deprecation notice title Please only use dashes as separator if the date format is ISO 8601 (YYYY-MM-DD) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1abbf0046..07c751b44 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ Crafted using base code from django-social-auth, it implements a common interfac to define new authentication providers from third parties, and to bring support for more frameworks and ORMs. -Deprecation notice - 03-12-2016 +Deprecation notice - 2016-12-03 ------------------------------- As for Dec 03 2016, this library is now deprecated, the codebase was From d7f556edb8ae2f129a4ac6cde31184c3c211acef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Thu, 26 Jan 2017 13:14:14 -0300 Subject: [PATCH 885/890] Version bump 0.3.4 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index c0376cb53..3f8b563d4 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 3, 3) +version = (0, 3, 4) extra = '' __version__ = '.'.join(map(str, version)) + extra From 51d3d67d5c657213a98af39d9c26ab0ec9c1af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 28 Jan 2017 10:44:56 -0300 Subject: [PATCH 886/890] Fix google openidconnect import --- social/backends/google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/backends/google.py b/social/backends/google.py index 32636a4c7..9baf3f486 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -1,3 +1,3 @@ from social_core.backends.google import BaseGoogleAuth, BaseGoogleOAuth2API, \ - GoogleOAuth2, GooglePlusAuth, GoogleOAuth, GoogleOpenId, \ - GoogleOpenIdConnect + GoogleOAuth2, GooglePlusAuth, GoogleOAuth, GoogleOpenId +from social_core.backends.google_openidconnect GoogleOpenIdConnect From 874e58c44679d96c40d7eba441e07631619b23b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 28 Jan 2017 10:45:51 -0300 Subject: [PATCH 887/890] Version bump 0.3.5 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 3f8b563d4..4dcbae552 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 3, 4) +version = (0, 3, 5) extra = '' __version__ = '.'.join(map(str, version)) + extra From f923691790086e6bd3ab79b93f39680e97286d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 3 Feb 2017 09:19:27 -0300 Subject: [PATCH 888/890] Fix import line --- social/backends/google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/backends/google.py b/social/backends/google.py index 9baf3f486..0f4070b73 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -1,3 +1,3 @@ from social_core.backends.google import BaseGoogleAuth, BaseGoogleOAuth2API, \ GoogleOAuth2, GooglePlusAuth, GoogleOAuth, GoogleOpenId -from social_core.backends.google_openidconnect GoogleOpenIdConnect +from social_core.backends.google_openidconnect import GoogleOpenIdConnect From cd1d086e79ba68d45ea47090cacee7754a140abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Fri, 3 Feb 2017 09:19:40 -0300 Subject: [PATCH 889/890] Version bump 0.3.6 --- social/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social/__init__.py b/social/__init__.py index 4dcbae552..fb6f35434 100644 --- a/social/__init__.py +++ b/social/__init__.py @@ -2,6 +2,6 @@ python-social-auth application, allows OpenId or OAuth user registration/authentication just adding a few configurations. """ -version = (0, 3, 5) +version = (0, 3, 6) extra = '' __version__ = '.'.join(map(str, version)) + extra From 81c0a542d158772bd3486d31834c10af5d5f08b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Feb 2017 10:10:59 -0300 Subject: [PATCH 890/890] Update migration doc about django migrations errors --- MIGRATING_TO_SOCIAL.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/MIGRATING_TO_SOCIAL.md b/MIGRATING_TO_SOCIAL.md index 265399c64..7ae5b8ac8 100644 --- a/MIGRATING_TO_SOCIAL.md +++ b/MIGRATING_TO_SOCIAL.md @@ -23,6 +23,37 @@ Django users need to add the `social-auth-app-django` dependency. Those using `mongoengine`, need to add `social-auth-app-mongoengine`. +### Migrations + +Several errors were reported due to migrations not applying properly +when migrating to the new app, most of them are caused because the app +switched names a few times, from `default` to `social_auth`, and +probably something else in between. That's the reason the migrations +define the `replaces` attribute, that way Django can identify already +applied migrations and not run them again. + +In order to make complete the move to the new project setup, first +ensure to move to `python-social-auth==0.2.21`, run the migrations at +that point, then continue with the move to the new project and run the +migrations again. Steps: + +1. Update to `0.2.21` + ``` + pip install "python-social-auth==0.2.21" + ``` + +2. Run migrations + ``` + python manage.py migrate + ``` + +3. Move to the new project + +4. Run migrations again + ``` + python manage.py migrate + ``` + ### Settings - Update your references to `social.*` in your settings, most notably: