[Python-modules-commits] [python-social-auth] 03/07: Import python-social-auth_0.2.21+dfsg.orig.tar.gz
Wolfgang Borgert
debacle at moszumanska.debian.org
Tue Jan 10 00:41:32 UTC 2017
This is an automated email from the git hooks/post-receive script.
debacle pushed a commit to branch master
in repository python-social-auth.
commit 02d4d7708972d1bf78a5ae590cfca8bdd6533293
Author: W. Martin Borgert <debacle at debian.org>
Date: Mon Jan 9 23:42:41 2017 +0000
Import python-social-auth_0.2.21+dfsg.orig.tar.gz
---
CHANGELOG.md | 94 +++++++++-
README.rst | 9 +-
docs/backends/battlenet.rst | 10 +-
docs/backends/edmodo.rst | 22 +++
docs/backends/facebook.rst | 2 +-
docs/backends/index.rst | 5 +
docs/backends/line.rst | 7 +
docs/backends/sketchfab.rst | 17 ++
docs/backends/twitter.rst | 5 +
docs/backends/untappd.rst | 28 +++
docs/backends/upwork.rst | 28 +++
docs/backends/vk.rst | 2 +-
docs/backends/yammer.rst | 3 +-
docs/intro.rst | 2 +
docs/pipeline.rst | 59 +++---
docs/thanks.rst | 2 +
docs/use_cases.rst | 5 -
.../cherrypy_example/local_settings.py.template | 1 +
examples/django_example/example/settings.py | 2 +
examples/django_me_example/example/settings.py | 2 +
examples/flask_example/__init__.py | 7 +-
examples/flask_example/models/user.py | 2 +-
examples/flask_example/routes/main.py | 2 +-
examples/flask_example/settings.py | 1 +
examples/flask_me_example/__init__.py | 6 +-
examples/flask_me_example/models/user.py | 2 +-
examples/flask_me_example/routes/main.py | 2 +-
examples/flask_me_example/settings.py | 1 +
.../__init__.py | 32 ++--
examples/flask_peewee_example/manage.py | 26 +++
examples/flask_peewee_example/models/__init__.py | 4 +
examples/flask_peewee_example/models/user.py | 24 +++
examples/flask_peewee_example/requirements.txt | 7 +
examples/flask_peewee_example/routes/__init__.py | 2 +
.../routes/main.py | 0
.../settings.py | 2 +-
examples/flask_peewee_example/templates/base.html | 14 ++
examples/flask_peewee_example/templates/done.html | 24 +++
examples/flask_peewee_example/templates/home.html | 85 +++++++++
examples/pyramid_example/example/settings.py | 1 +
examples/tornado_example/settings.py | 1 +
examples/webpy_example/app.py | 1 +
site/css/site.css | 4 +
site/docs | 1 +
site/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes
site/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes
site/index.html | 104 +++++++++++
social/__init__.py | 2 +-
social/actions.py | 17 +-
social/apps/django_app/default/admin.py | 16 +-
social/apps/django_app/default/config.py | 1 +
social/apps/django_app/default/fields.py | 9 +-
.../django_app/default/migrations/0001_initial.py | 2 +
.../default/migrations/0002_add_related_name.py | 11 +-
.../migrations/0003_alter_email_max_length.py | 4 +-
.../default/migrations/0004_auto_20160423_0400.py | 3 +-
.../default/migrations/0005_auto_20160727_2333.py | 19 ++
social/apps/django_app/default/models.py | 3 +
social/apps/django_app/tests.py | 1 +
social/apps/flask_app/peewee/__init__.py | 0
social/apps/flask_app/peewee/models.py | 48 +++++
social/apps/flask_app/routes.py | 2 +-
social/apps/pyramid_app/models.py | 2 +
social/apps/pyramid_app/views.py | 2 +-
social/backends/battlenet.py | 2 +-
social/backends/coding.py | 48 +++++
social/backends/coursera.py | 4 +
social/backends/dribbble.py | 2 +-
social/backends/edmodo.py | 34 ++++
social/backends/facebook.py | 8 +-
social/backends/line.py | 91 ++++++++++
social/backends/oauth.py | 10 ++
social/backends/qiita.py | 2 +-
social/backends/reddit.py | 6 +-
social/backends/sketchfab.py | 39 ++++
social/backends/uber.py | 16 +-
social/backends/untappd.py | 110 ++++++++++++
social/backends/upwork.py | 39 ++++
social/backends/weixin.py | 78 +++++++-
social/pipeline/social_auth.py | 5 +-
social/pipeline/user.py | 4 +-
social/storage/django_orm.py | 29 ++-
social/storage/peewee_orm.py | 199 +++++++++++++++++++++
social/storage/sqlalchemy_orm.py | 8 +-
social/strategies/base.py | 2 +
social/strategies/pyramid_strategy.py | 8 +-
social/tests/backends/test_edmodo.py | 44 +++++
social/tests/backends/test_facebook.py | 2 +-
social/tests/backends/test_sketchfab.py | 26 +++
social/tests/backends/test_twitter.py | 129 +++++++++++++
social/tests/backends/test_upwork.py | 53 ++++++
social/tests/test_utils.py | 52 +++++-
social/utils.py | 27 ++-
93 files changed, 1742 insertions(+), 137 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f65d2a8..c16a9aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,94 @@
# Change Log
-## [v0.2.17](https://github.com/omab/python-social-auth/tree/v0.2.17) (2016-04-20)
+## [Unreleased](https://github.com/omab/python-social-auth/tree/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:**
+- 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:**
+
+- \[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:**
+
+- 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))
+
+## [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:**
+- 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)
@@ -29,6 +112,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))
@@ -40,6 +126,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))
@@ -68,6 +155,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))
@@ -410,7 +498,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))
@@ -927,7 +1015,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/README.rst b/README.rst
index ec4e25a..f2b0516 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
@@ -84,6 +81,7 @@ or current ones extended):
* Kakao_ OAuth2 https://developer.kakao.com
* `Khan Academy`_ OAuth1
* Launchpad_ OpenId
+ * Line_ OAuth2
* Linkedin_ OAuth1
* Live_ OAuth2
* Livejournal_ OpenId
@@ -108,6 +106,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
@@ -123,6 +122,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
@@ -263,6 +263,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
@@ -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
@@ -325,3 +327,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/battlenet.rst b/docs/backends/battlenet.rst
index 65a1ac0..db6a842 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
diff --git a/docs/backends/edmodo.rst b/docs/backends/edmodo.rst
new file mode 100644
index 0000000..095c456
--- /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/facebook.rst b/docs/backends/facebook.rst
index cf91365..894070b 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']
diff --git a/docs/backends/index.rst b/docs/backends/index.rst
index 7e3a235..37c29bb 100644
--- a/docs/backends/index.rst
+++ b/docs/backends/index.rst
@@ -71,6 +71,7 @@ Social backends
dribbble
drip
dropbox
+ edmodo
eveonline
evernote
facebook
@@ -89,6 +90,7 @@ Social backends
khanacademy
lastfm
launchpad
+ line
linkedin
livejournal
live
@@ -120,6 +122,7 @@ Social backends
runkeeper
salesforce
shopify
+ sketchfab
skyrock
slack
soundcloud
@@ -139,6 +142,8 @@ Social backends
twitch
twitter
uber
+ untappd
+ upwork
vend
vimeo
vk
diff --git a/docs/backends/line.rst b/docs/backends/line.rst
new file mode 100644
index 0000000..f63c611
--- /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/docs/backends/sketchfab.rst b/docs/backends/sketchfab.rst
new file mode 100644
index 0000000..c69a388
--- /dev/null
+++ b/docs/backends/sketchfab.rst
@@ -0,0 +1,17 @@
+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/twitter.rst b/docs/backends/twitter.rst
index e40c413..64e885c 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 (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
@@ -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
diff --git a/docs/backends/untappd.rst b/docs/backends/untappd.rst
new file mode 100644
index 0000000..0b41a12
--- /dev/null
+++ b/docs/backends/untappd.rst
@@ -0,0 +1,28 @@
+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 = '<App UID>'
+ SOCIAL_AUTH_UNTAPPD_SECRET = '<App 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
new file mode 100644
index 0000000..59b5990
--- /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 = ''
+
+
+**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/vk.rst b/docs/backends/vk.rst
index 2b5deb5..a9195c7 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.
diff --git a/docs/backends/yammer.rst b/docs/backends/yammer.rst
index df6104d..86f4c74 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://<your hostname>/complete/yammer/``
- Fill **Client Key** and **Client Secret** settings::
diff --git a/docs/intro.rst b/docs/intro.rst
index 12a45cb..af3e37c 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
diff --git a/docs/pipeline.rst b/docs/pipeline.rst
index bd65660..b3efc48 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 ``{}``
@@ -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=<code here>``, 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=<code here>``. 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
diff --git a/docs/thanks.rst b/docs/thanks.rst
index ca4a40e..46d7a20 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/docs/use_cases.rst b/docs/use_cases.rst
index 3ac5856..4f7c926 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/
diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template
index 98c8f8f..7209aca 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 62f2b5b..9077b5a 100644
--- a/examples/django_example/example/settings.py
+++ b/examples/django_example/example/settings.py
@@ -174,6 +174,7 @@ AUTHENTICATION_BACKENDS = (
'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',
@@ -203,6 +204,7 @@ AUTHENTICATION_BACKENDS = (
'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 7dcdfad..88783b3 100644
--- a/examples/django_me_example/example/settings.py
+++ b/examples/django_me_example/example/settings.py
@@ -173,6 +173,7 @@ AUTHENTICATION_BACKENDS = (
'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',
@@ -182,6 +183,7 @@ AUTHENTICATION_BACKENDS = (
'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/__init__.py b/examples/flask_example/__init__.py
old mode 100644
new mode 100755
index d24fb82..07c1080
--- a/examples/flask_example/__init__.py
+++ b/examples/flask_example/__init__.py
@@ -4,7 +4,7 @@ from sqlalchemy import create_engine
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 @@ db_session = scoped_session(Session)
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,8 @@ def load_user(userid):
@app.before_request
def global_user():
- g.user = login.current_user
+ # evaluate proxy value
+ g.user = current_user._get_current_object()
@app.teardown_appcontext
diff --git a/examples/flask_example/models/user.py b/examples/flask_example/models/user.py
index 7cb7058..08bcee8 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 5e0dd9c..fd65261 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_example/settings.py b/examples/flask_example/settings.py
index 0abaa91..6b5324c 100644
--- a/examples/flask_example/settings.py
+++ b/examples/flask_example/settings.py
@@ -52,4 +52,5 @@ SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
'social.backends.reddit.RedditOAuth2',
'social.backends.mineid.MineIDOAuth2',
'social.backends.wunderlist.WunderlistOAuth2',
+ 'social.backends.upwork.UpworkOAuth',
)
diff --git a/examples/flask_me_example/__init__.py b/examples/flask_me_example/__init__.py
index f838a20..9f5aaac 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 @@ db = MongoEngine(app)
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
diff --git a/examples/flask_me_example/models/user.py b/examples/flask_me_example/models/user.py
index 035b527..d351fed 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 127986e..d2cfd82 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/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py
index e4f2338..cf4ed21 100644
--- a/examples/flask_me_example/settings.py
+++ b/examples/flask_me_example/settings.py
@@ -58,4 +58,5 @@ SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
'social.backends.reddit.RedditOAuth2',
'social.backends.mineid.MineIDOAuth2',
'social.backends.wunderlist.WunderlistOAuth2',
+ 'social.backends.upwork.UpworkOAuth',
)
diff --git a/examples/flask_example/__init__.py b/examples/flask_peewee_example/__init__.py
similarity index 58%
copy from examples/flask_example/__init__.py
copy to examples/flask_peewee_example/__init__.py
index d24fb82..4c2543f 100644
--- a/examples/flask_example/__init__.py
+++ b/examples/flask_peewee_example/__init__.py
@@ -1,8 +1,5 @@
import sys
-from sqlalchemy import create_engine
-from sqlalchemy.orm import scoped_session, sessionmaker
-
from flask import Flask, g
from flask.ext import login
@@ -10,7 +7,8 @@ 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
+from social.apps.flask_app.peewee.models import *
+from peewee import *
# App
app = Flask(__name__)
@@ -21,13 +19,14 @@ try:
except ImportError:
pass
+from models.user import database_proxy, User
+
# DB
-engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'])
-Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
-db_session = scoped_session(Session)
+database = SqliteDatabase('test.db')
+database_proxy.initialize(database)
app.register_blueprint(social_auth)
-init_social(app, db_session)
+init_social(app, database)
login_manager = login.LoginManager()
login_manager.login_view = 'main'
@@ -41,24 +40,15 @@ 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):
+ us = User.get(User.id == userid)
+ return us
+ except User.DoesNotExist:
pass
@app.before_request
def global_user():
- g.user = login.current_user
-
-
- at app.teardown_appcontext
-def commit_on_success(error=None):
- if error is None:
- db_session.commit()
- else:
- db_session.rollback()
-
- db_session.remove()
+ g.user = login.current_user._get_current_object()
@app.context_processor
diff --git a/examples/flask_peewee_example/manage.py b/examples/flask_peewee_example/manage.py
new file mode 100644
index 0000000..9270586
--- /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
+}))
+
+
+ at 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 0000000..2253824
--- /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 0000000..a46f559
--- /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 0000000..e52656b
--- /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 0000000..d3586e8
--- /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_example/routes/main.py b/examples/flask_peewee_example/routes/main.py
similarity index 100%
copy from examples/flask_example/routes/main.py
copy to examples/flask_peewee_example/routes/main.py
diff --git a/examples/flask_example/settings.py b/examples/flask_peewee_example/settings.py
similarity index 96%
copy from examples/flask_example/settings.py
copy to examples/flask_peewee_example/settings.py
index 0abaa91..419c575 100644
--- a/examples/flask_example/settings.py
+++ b/examples/flask_peewee_example/settings.py
@@ -3,10 +3,10 @@ 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_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'
diff --git a/examples/flask_peewee_example/templates/base.html b/examples/flask_peewee_example/templates/base.html
new file mode 100644
index 0000000..86db504
--- /dev/null
+++ b/examples/flask_peewee_example/templates/base.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Social</title>
+ <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet" media="screen">
+ </head>
+ <body>
+ {% block content %}{% endblock %}
+ {% block scripts %}{% endblock %}
+
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
+ <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js" type="text/javascript"></script>
+ </body>
+</html>
diff --git a/examples/flask_peewee_example/templates/done.html b/examples/flask_peewee_example/templates/done.html
new file mode 100644
index 0000000..ccabf53
--- /dev/null
+++ b/examples/flask_peewee_example/templates/done.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+
+{% block content %}
+<p>You are logged in as {{ user.username }}!</p>
+
+<p>Associated:</p>
+{% for assoc in backends.associated %}
+ <div>
+ {{ assoc.provider }}
+ <form method="post" action="{{ url_for("social.disconnect", backend=assoc.provider, association_id=assoc.id) }}">
+ <button>Disconnect</button>
+ </form>
+ </div>
+{% endfor %}
+
+<p>Associate:</p>
+<ul>
+ {% for name in backends.not_associated %}
+ <li>
+ <a href="{{ url_for("social.auth", backend=name) }}">{{ name }}</a>
+ </li>
+ {% endfor %}
+</ul>
+{% endblock %}
diff --git a/examples/flask_peewee_example/templates/home.html b/examples/flask_peewee_example/templates/home.html
new file mode 100644
index 0000000..1c7f9bc
--- /dev/null
+++ b/examples/flask_peewee_example/templates/home.html
@@ -0,0 +1,85 @@
+{% extends "base.html" %}
+
+{% block content %}
+<a href="{{ url_for("social.auth", backend="google-oauth2") }}">Google OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="google-oauth") }}">Google OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="google") }}">Google OpenId</a> <br />
+<a href="{{ url_for("social.auth", backend="twitter") }}">Twitter OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="yahoo") }}">Yahoo OpenId</a> <br />
+<a href="{{ url_for("social.auth", backend="yahoo-oauth") }}">Yahoo OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="stripe") }}">Stripe OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="facebook") }}">Facebook OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="facebook-app") }}">Facebook App</a> <br />
+<a href="{{ url_for("social.auth", backend="angel") }}">Angel OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="behance") }}">Behance OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="bitbucket") }}">Bitbucket OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="box") }}">Box OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="linkedin") }}">LinkedIn OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="github") }}">Github OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="foursquare") }}">Foursquare OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="instagram") }}">Instagram OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="live") }}">Live OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="vk-oauth2") }}">VK.com OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="dailymotion") }}">Dailymotion OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="disqus") }}">Disqus OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="dropbox") }}">Dropbox OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="evernote-sandbox") }}">Evernote OAuth (sandbox mode)</a> <br />
+<a href="{{ url_for("social.auth", backend="fitbit") }}">Fitbit OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="flickr") }}">Flickr OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="soundcloud") }}">Soundcloud OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="thisismyjam") }}">ThisIsMyJam OAuth1</a> <br />
+<a href="{{ url_for("social.auth", backend="stocktwits") }}">Stocktwits OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="tripit") }}">Tripit OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="clef") }}">Clef OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="twilio") }}">Twilio</a> <br />
+<a href="{{ url_for("social.auth", backend="xing") }}">Xing OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="yandex-oauth2") }}">Yandex OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="podio") }}">Podio OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="mineid") }}">MineID OAuth2</a> <br />
+
+<form action="{{ url_for("social.auth", backend="openid") }}" method="post">
+ <div>
+ <label for="openid_identifier">OpenId provider</label>
+ <input id="openid_identifier" type="text" value="" name="openid_identifier" />
+ <input type="submit" value="Login" />
+ </div>
+</form>
+
+<form action="{{ url_for("social.auth", backend="livejournal") }}" method="post">
+ <div>
+ <label for="openid_lj_identifier">LiveJournal ID</label>
+ <input id="openid_lj_identifier" type="text" value="" name="openid_lj_user" />
+ <input type="submit" value="Login" />
+ </div>
+</form>
+
+<form method="post" action="{{ url_for("social.complete", backend="persona") }}">
+ <input type="hidden" name="assertion" value="" />
+ <a rel="nofollow" id="persona" href="#">Persona</a>
+</form>
+{% endblock %}
+
+{% block scripts %}
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js" type="text/javascript"></script>
+<script src="https://login.persona.org/include.js" type="text/javascript"></script>
+<script type="text/javascript">
+$(function () {
+ $('#persona').on('click', function (e) {
+ e.preventDefault();
+ var self = $(this);
+
+ navigator.id.get(function (assertion) {
+ if (assertion) {
+ self.parent('form')
+ .find('input[type=hidden]')
+ .attr('value', assertion)
+ .end()
+ .submit();
+ } else {
+ alert('Some error occurred');
+ }
+ });
+ });
+});
+</script>
+{% endblock %}
diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py
index 35a69ad..05e2d9e 100644
--- a/examples/pyramid_example/example/settings.py
+++ b/examples/pyramid_example/example/settings.py
@@ -46,6 +46,7 @@ SOCIAL_AUTH_SETTINGS = {
'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 ff6b146..81ae2eb 100644
--- a/examples/tornado_example/settings.py
+++ b/examples/tornado_example/settings.py
@@ -45,6 +45,7 @@ SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
'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 4109fac..2224d93 100644
--- a/examples/webpy_example/app.py
+++ b/examples/webpy_example/app.py
@@ -57,6 +57,7 @@ web.config[setting_name('AUTHENTICATION_BACKENDS')] = (
'social.backends.podio.PodioOAuth2',
'social.backends.mineid.MineIDOAuth2',
'social.backends.wunderlist.WunderlistOAuth2',
+ 'social.backends.upwork.UpworkOAuth',
)
web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/'
diff --git a/site/css/site.css b/site/css/site.css
new file mode 100644
index 0000000..0292b3b
--- /dev/null
+++ b/site/css/site.css
@@ -0,0 +1,4 @@
+body {
+ padding-top: 60px;
+ padding-bottom: 40px;
+}
diff --git a/site/docs b/site/docs
new file mode 120000
index 0000000..16bfa41
--- /dev/null
+++ b/site/docs
@@ -0,0 +1 @@
+../docs/_build/
\ No newline at end of file
diff --git a/site/img/glyphicons-halflings-white.png b/site/img/glyphicons-halflings-white.png
new file mode 100644
index 0000000..3bf6484
Binary files /dev/null and b/site/img/glyphicons-halflings-white.png differ
diff --git a/site/img/glyphicons-halflings.png b/site/img/glyphicons-halflings.png
new file mode 100644
index 0000000..a996999
Binary files /dev/null and b/site/img/glyphicons-halflings.png differ
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..230537b
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Python Social Auth</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
+ <link href="css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
+ <link href="css/site.css" rel="stylesheet" media="screen">
+ </head>
+ <body>
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <a class="brand" href="#">Python Social Auth</a>
+
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+ <li class="active"><a href="#">Home</a></li>
+ <li><a href="docs/index.html">Documentation</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="container">
+ <div class="hero-unit">
+ <h1>Python Social Auth</h1>
+ <p>
+ Python Social Auth is an easy to setup social authentication/registration
+ mechanism with support for several frameworks and auth providers.
+ </p>
+
+ <p>
+ 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.
+ </p>
+ <p><a href="docs/index.html" class="btn btn-primary btn-large">Learn more »</a></p>
+ </div>
+
+ <div class="row">
+ <div class="span6">
+ <h2>Frameworks</h2>
+ <p>
+ The lib supports a few frameworks at the moment with <a href="http://djangoproject.com/">Django</a>,
+ <a href="http://flask.pocoo.org/">Flask</a>, <a href="http://www.pylonsproject.org/projects/pyramid/about">Pyramid</a>,
+ <a href="http://webpy.org/">Webpy</a>, <a href="http://www.cherrypy.org/">CherryPy</a> and
+ <a href="http://www.tornadoweb.org/">Tornado</a> and more to come. The <a href="docs/strategies.html">frameworks API</a>
+ should ease the implementation to increase the number of frameworks supported.
+ </p>
+ <p><a class="btn" href="docs/strategies.html">View details »</a></p>
+ </div>
+
+ <div class="span6">
+ <h2>Authentication Providers</h2>
+ <p>
+ Ported from <a href="https://github.com/omab/django-social-auth">django-social-auth</a>, the application
+ brings plenty of authentication providers, many from popular services like <a href="docs/backends/google.html">Google</a>,
+ <a href="docs/backends/facebook.html">Facebook</a>, <a href="docs/backends/twitter.html">Twitter</a> and
+ <a href="docs/backends/github.html">Github</a>. The <a href="docs/backends/implementation.html">backends API</a>
+ have some implementation details on how to implement your own backends.
+ </p>
+ <p><a class="btn" href="docs/backends/index.html">View details »</a></p>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="span6">
+ <h2>ORMs</h2>
+ <p>
+ There are <a href="http://wiki.python.org/moin/HigherLevelDatabaseProgramming">multiple ORM python libraries</a> around,
+ some frameworks has their own built-in version too. <a href="https://github.com/omab/python-social-auth">python-social-auth</a>
+ tries to support the different interfaces available, at the moment <a href="http://www.sqlalchemy.org/">SQLAlchemy</a>,
+ <a href="https://docs.djangoproject.com/en/dev/topics/db/">Django ORM</a> and <a href="http://mongoengine.org/">Mongoengine</a>
+ are supported, but with the <a href="docs/storage.html">Storage API</a> it should be easy to add more support.
+ </p>
+ <p><a class="btn" href="docs/storage.html">View details »</a></p>
+ </div>
+
+ <div class="span6">
+ <h2>Development and Contact</h2>
+ <p>
+ The code is available on <a href="https://github.com/omab/python-social-auth">Github</a>, report any
+ <a href="https://github.com/omab/python-social-auth/issues">issue</a> if you find any. Pull requests are
+ always welcome. There's a <a href="https://groups.google.com/forum/?fromgroups#!forum/python-social-auth">mailing list</a>
+ and IRC channel <code>#python-social-auth</code> on Freenode network.
+ </p>
+ <p><a class="btn" href="https://github.com/omab/python-social-auth">View details »</a></p>
+ </div>
+ </div>
+
+ <hr />
+
+ <footer>
+ <p>© Matías Aguirre 2012</p>
+ </footer>
+ </div>
+
+ <script src="http://code.jquery.com/jquery.js"></script>
+ <script src="js/bootstrap.min.js"></script>
+ </body>
+</html>
diff --git a/social/__init__.py b/social/__init__.py
index 895ff9a..8766fd5 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, 21)
extra = ''
__version__ = '.'.join(map(str, version)) + extra
diff --git a/social/actions.py b/social/actions.py
index 4b005cf..af61bd7 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,7 +92,9 @@ 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 \
+ 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)
@@ -110,8 +113,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
diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py
index de7802b..5cee753 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
@@ -24,11 +26,23 @@ class UserSocialAuthOption(admin.ModelAdmin):
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):
+ 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"""
diff --git a/social/apps/django_app/default/config.py b/social/apps/django_app/default/config.py
index 745e24d..a2c44b7 100644
--- a/social/apps/django_app/default/config.py
+++ b/social/apps/django_app/default/config.py
@@ -3,6 +3,7 @@ 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):
diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py
index 8593386..ab47fba 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 @@ try:
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.
"""
diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py
index 9917f67..6766d92 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 @@ ASSOCIATION_HANDLE_LENGTH = getattr(
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 8e39f15..8a791ea 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,17 +4,24 @@ 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 = [
- ('default', '0001_initial'),
+ ('social_auth', '0001_initial'),
]
operations = [
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)
),
]
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 882c311..3557d70 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,8 +10,10 @@ EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254)
class Migration(migrations.Migration):
+ replaces = [('default', '0003_alter_email_max_length')]
+
dependencies = [
- ('default', '0002_add_related_name'),
+ ('social_auth', '0002_add_related_name'),
]
operations = [
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 668bf0e..82648bc 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,9 +6,10 @@ import social.apps.django_app.default.fields
class Migration(migrations.Migration):
+ replaces = [('default', '0004_auto_20160423_0400')]
dependencies = [
- ('default', '0003_alter_email_max_length'),
+ ('social_auth', '0003_alter_email_max_length'),
]
operations = [
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 0000000..3df56ef
--- /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 9f18529..047da91 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):
diff --git a/social/apps/django_app/tests.py b/social/apps/django_app/tests.py
index 022d7fc..1dec95e 100644
--- a/social/apps/django_app/tests.py
+++ b/social/apps/django_app/tests.py
@@ -31,6 +31,7 @@ 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 *
diff --git a/social/apps/flask_app/peewee/__init__.py b/social/apps/flask_app/peewee/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py
new file mode 100644
index 0000000..497e9a5
--- /dev/null
+++ b/social/apps/flask_app/peewee/models.py
@@ -0,0 +1,48 @@
+"""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
diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py
index 6c1eec7..b3b1406 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
diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py
index 2d70e06..5752f3f 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/apps/pyramid_app/views.py b/social/apps/pyramid_app/views.py
index 38ee4fa..7587ff9 100644
--- a/social/apps/pyramid_app/views.py
+++ b/social/apps/pyramid_app/views.py
@@ -5,7 +5,7 @@ from social.actions import do_auth, do_complete, do_disconnect
from social.apps.pyramid_app.utils import psa, login_required
- at view_config(route_name='social.auth', request_method='GET')
+ at view_config(route_name='social.auth', request_method=('GET', 'POST'))
@psa('social.complete')
def auth(request):
return do_auth(request.backend, redirect_name='next')
diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py
index ecf77a1..eefd2fa 100644
--- a/social/backends/battlenet.py
+++ b/social/backends/battlenet.py
@@ -45,6 +45,6 @@ class BattleNetOAuth2(BaseOAuth2):
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}
)
diff --git a/social/backends/coding.py b/social/backends/coding.py
new file mode 100644
index 0000000..3b4295e
--- /dev/null
+++ b/social/backends/coding.py
@@ -0,0 +1,48 @@
+"""
+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})
diff --git a/social/backends/coursera.py b/social/backends/coursera.py
index 9507edb..ed32721 100644
--- a/social/backends/coursera.py
+++ b/social/backends/coursera.py
@@ -28,6 +28,10 @@ class CourseraOAuth2(BaseOAuth2):
"""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(
diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py
index e1c3eee..ab6d0e5 100644
--- a/social/backends/dribbble.py
+++ b/social/backends/dribbble.py
@@ -58,5 +58,5 @@ class DribbbleOAuth2(BaseOAuth2):
return self.get_json(
'https://api.dribbble.com/v1/user',
headers={
- 'Authorization': ' Bearer {0}'.format(access_token)
+ 'Authorization': 'Bearer {0}'.format(access_token)
})
diff --git a/social/backends/edmodo.py b/social/backends/edmodo.py
new file mode 100644
index 0000000..cc73589
--- /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/backends/facebook.py b/social/backends/facebook.py
index f76b502..f904556 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.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.3/me'
+ USER_DATA_URL = 'https://graph.facebook.com/v2.7/me'
EXTRA_DATA = [
('id', 'id'),
('expires', 'expires')
diff --git a/social/backends/line.py b/social/backends/line.py
new file mode 100644
index 0000000..ba8136e
--- /dev/null
+++ b/social/backends/line.py
@@ -0,0 +1,91 @@
+# 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())
diff --git a/social/backends/oauth.py b/social/backends/oauth.py
index b2210ec..3182e52 100644
--- a/social/backends/oauth.py
+++ b/social/backends/oauth.py
@@ -357,6 +357,16 @@ class BaseOAuth2(OAuthAuth):
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)
diff --git a/social/backends/qiita.py b/social/backends/qiita.py
index 52b947a..92bcbd3 100644
--- a/social/backends/qiita.py
+++ b/social/backends/qiita.py
@@ -62,5 +62,5 @@ class QiitaOAuth2(BaseOAuth2):
return self.get_json(
'https://qiita.com/api/v2/authenticated_user',
headers={
- 'Authorization': ' Bearer {0}'.format(access_token)
+ 'Authorization': 'Bearer {0}'.format(access_token)
})
diff --git a/social/backends/reddit.py b/social/backends/reddit.py
index 62ac42c..712bdfb 100644
--- a/social/backends/reddit.py
+++ b/social/backends/reddit.py
@@ -42,9 +42,9 @@ class RedditOAuth2(BaseOAuth2):
def auth_headers(self):
return {
- 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode(
- ('{0}:{1}'.format(*self.get_key_and_secret()).encode())
- ))
+ '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):
diff --git a/social/backends/sketchfab.py b/social/backends/sketchfab.py
new file mode 100644
index 0000000..cb19ef5
--- /dev/null
+++ b/social/backends/sketchfab.py
@@ -0,0 +1,39 @@
+"""
+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/backends/uber.py b/social/backends/uber.py
index ef6c381..6b1463b 100644
--- a/social/backends/uber.py
+++ b/social/backends/uber.py
@@ -19,7 +19,11 @@ class UberOAuth2(BaseOAuth2):
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,
@@ -28,12 +32,8 @@ class UberOAuth2(BaseOAuth2):
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)
+ })
diff --git a/social/backends/untappd.py b/social/backends/untappd.py
new file mode 100644
index 0000000..7d241ad
--- /dev/null
+++ b/social/backends/untappd.py
@@ -0,0 +1,110 @@
+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']
diff --git a/social/backends/upwork.py b/social/backends/upwork.py
new file mode 100644
index 0000000..64f4deb
--- /dev/null
+++ b/social/backends/upwork.py
@@ -0,0 +1,39 @@
+"""
+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)
+ )
diff --git a/social/backends/weixin.py b/social/backends/weixin.py
index 1b5c2d5..279a030 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
@@ -43,7 +44,9 @@ class WeixinOAuth2(BaseOAuth2):
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):
@@ -99,3 +102,76 @@ class WeixinOAuth2(BaseOAuth2):
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)
diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py
index 87895ec..b181ba5 100644
--- a/social/pipeline/social_auth.py
+++ b/social/pipeline/social_auth.py
@@ -27,7 +27,7 @@ def social_user(backend, uid, user=None, *args, **kwargs):
return {'social': social,
'user': user,
'is_new': user is None,
- 'new_association': False}
+ 'new_association': social is None}
def associate_user(backend, uid, user=None, social=None, *args, **kwargs):
@@ -76,7 +76,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):
diff --git a/social/pipeline/user.py b/social/pipeline/user.py
index a46fc3e..e5f8d65 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
diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py
index 8d9e672..e331656 100644
--- a/social/storage/django_orm.py
+++ b/social/storage/django_orm.py
@@ -1,6 +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
@@ -57,7 +60,20 @@ class DjangoUserMixin(UserMixin):
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):
@@ -96,7 +112,16 @@ class DjangoUserMixin(UserMixin):
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):
diff --git a/social/storage/peewee_orm.py b/social/storage/peewee_orm.py
new file mode 100644
index 0000000..60b5da0
--- /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):
+ 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
diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py
index 621bf4c..e48ef64 100644
--- a/social/storage/sqlalchemy_orm.py
+++ b/social/storage/sqlalchemy_orm.py
@@ -28,6 +28,8 @@ class JSONType(PickleType):
class SQLAlchemyMixin(object):
+ COMMIT_SESSION = True
+
@classmethod
def _session(cls):
raise NotImplementedError('Implement in subclass')
@@ -43,7 +45,11 @@ class SQLAlchemyMixin(object):
@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
diff --git a/social/strategies/base.py b/social/strategies/base.py
index ce66af0..22f24e1 100644
--- a/social/strategies/base.py
+++ b/social/strategies/base.py
@@ -131,6 +131,8 @@ class BaseStrategy(object):
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
diff --git a/social/strategies/pyramid_strategy.py b/social/strategies/pyramid_strategy.py
index 6ac4157..9761a42 100644
--- a/social/strategies/pyramid_strategy.py
+++ b/social/strategies/pyramid_strategy.py
@@ -38,13 +38,7 @@ class PyramidStrategy(BaseStrategy):
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)"""
diff --git a/social/tests/backends/test_edmodo.py b/social/tests/backends/test_edmodo.py
new file mode 100644
index 0000000..ff3914c
--- /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 at 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()
diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py
index 166d753..81146d9 100644
--- a/social/tests/backends/test_facebook.py
+++ b/social/tests/backends/test_facebook.py
@@ -7,7 +7,7 @@ from social.tests.backends.oauth import OAuth2Test
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.7/me'
expected_username = 'foobar'
access_token_body = json.dumps({
'access_token': 'foobar',
diff --git a/social/tests/backends/test_sketchfab.py b/social/tests/backends/test_sketchfab.py
new file mode 100644
index 0000000..ada632f
--- /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 at 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()
diff --git a/social/tests/backends/test_twitter.py b/social/tests/backends/test_twitter.py
index 4300237..fc2af14 100644
--- a/social/tests/backends/test_twitter.py
+++ b/social/tests/backends/test_twitter.py
@@ -129,3 +129,132 @@ class TwitterOAuth1Test(OAuth1Test):
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 at bar.bas'
+ })
+
+ def test_login(self):
+ user = self.do_login()
+ self.assertEquals(user.email, 'foo at bar.bas')
+
+ def test_partial_pipeline(self):
+ self.do_partial_pipeline()
diff --git a/social/tests/backends/test_upwork.py b/social/tests/backends/test_upwork.py
new file mode 100644
index 0000000..8eb6d8d
--- /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()
diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py
index 7bd8db0..4a13ba3 100644
--- a/social/tests/test_utils.py
+++ b/social/tests/test_utils.py
@@ -13,31 +13,41 @@ PY3 = sys.version_info[0] == 3
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):
@@ -121,6 +131,30 @@ class BuildAbsoluteURITest(unittest.TestCase):
class PartialPipelineData(unittest.TestCase):
+ def test_returns_partial_when_uid_and_email_do_match(self):
+ email = 'foo at 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 at example.com'})
+ backend.strategy.request_data.return_value = {
+ backend.ID_KEY: 'bar at 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 +162,7 @@ class PartialPipelineData(unittest.TestCase):
*(), **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 +170,18 @@ class PartialPipelineData(unittest.TestCase):
_, 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 0b5a507..f405375 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 that's not in the list
+ netloc = urlparse(redirect_to)[1] or hosts[0]
except (TypeError, AttributeError):
pass
else:
- if netloc == host:
+ if netloc in hosts:
return redirect_to
@@ -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)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-social-auth.git
More information about the Python-modules-commits
mailing list