]> git.parisson.com Git - django-social-auth.git/commitdiff
Merging latest changes from omab
authorStas Kravets <krvss@mail.ru>
Thu, 16 Feb 2012 10:00:32 +0000 (14:00 +0400)
committerStas Kravets <krvss@mail.ru>
Thu, 16 Feb 2012 10:00:32 +0000 (14:00 +0400)
52 files changed:
.gitignore
README.rst
doc/backends/browserid.rst [new file with mode: 0644]
doc/backends/github.rst
doc/backends/google.rst
doc/backends/index.rst
doc/backends/instagram.rst [new file with mode: 0644]
doc/configuration.rst
doc/deprecated.rst [new file with mode: 0644]
doc/index.rst
doc/intro.rst
doc/miscellaneous.rst
doc/pipeline.rst [new file with mode: 0644]
doc/use_cases.rst [new file with mode: 0644]
example/app/pipeline.py [new file with mode: 0644]
example/app/views.py
example/local_settings.py.template
example/settings.py
example/templates/base.html
example/templates/done.html
example/templates/form.html [new file with mode: 0644]
example/templates/home.html
example/urls.py
social_auth/__init__.py
social_auth/backends/__init__.py
social_auth/backends/browserid.py [new file with mode: 0644]
social_auth/backends/contrib/dropbox.py
social_auth/backends/contrib/flickr.py
social_auth/backends/contrib/foursquare.py
social_auth/backends/contrib/github.py
social_auth/backends/contrib/instagram.py [new file with mode: 0644]
social_auth/backends/contrib/linkedin.py
social_auth/backends/contrib/livejournal.py
social_auth/backends/contrib/orkut.py
social_auth/backends/facebook.py
social_auth/backends/google.py
social_auth/backends/pipeline/__init__.py
social_auth/backends/pipeline/associate.py
social_auth/backends/pipeline/misc.py [new file with mode: 0644]
social_auth/backends/pipeline/social.py
social_auth/backends/pipeline/user.py
social_auth/backends/twitter.py
social_auth/backends/yahoo.py
social_auth/context_processors.py
social_auth/models.py
social_auth/tests/__init__.py
social_auth/tests/facebook.py
social_auth/tests/google.py
social_auth/tests/twitter.py
social_auth/urls.py
social_auth/utils.py
social_auth/views.py

index bee6585ad2be9d8f9e0d502bf47600471ff34dac..be0304f88feb2aa04571d609386a9d748ac31336 100644 (file)
@@ -10,3 +10,6 @@ env/
 doc/_build
 example/templates/script*.html
 contrib/tests/test_settings.py
+*.ipr
+*.iws
+Django-social-auth.iml
index 007aeb0a11ca6f676c59f2f70ec8248feba8b7b1..e7d1cfb61d96b3c273f223090940ade631c1508e 100644 (file)
@@ -1,4 +1,3 @@
-==================
 Django Social Auth
 ==================
 
@@ -13,17 +12,18 @@ You can check this documentation on `Read the Docs`_ too.
 
 For Russian services backends (Yandex, Mail.ru, etc.) see http://www.ikrvss.ru/tag/django-social-auth/
 
+.. contents:: Table of Contents
+
 
-----
 Demo
 ----
+
 There's a demo at http://social.matiasaguirre.net/.
 Note: It lacks some backends support at the moment.
 
-
---------
 Features
 --------
+
 This application provides user registration and login using social sites
 credentials, some features are:
 
@@ -55,10 +55,11 @@ credentials, some features are:
 
 - Custom User model override if needed (`auth.User`_ by default)
 
+- Extensible pipeline to handle authentication/association mechanism
 
-------------
 Dependencies
 ------------
+
 Dependencies that **must** be meet to use the application:
 
 - OpenId_ support depends on python-openid_
@@ -68,8 +69,6 @@ Dependencies that **must** be meet to use the application:
 - Several backends demands application registration on their corresponding
   sites.
 
-
-------------
 Installation
 ------------
 
@@ -94,10 +93,9 @@ or::
     $ cd django-social-auth
     $ sudo python setup.py install
 
-
--------------
 Configuration
 -------------
+
 - Add social_auth to ``PYTHONPATH`` and installed applications::
 
     INSTALLED_APPS = (
@@ -114,6 +112,7 @@ Configuration
         'social_auth.backends.google.GoogleOAuth2Backend',
         'social_auth.backends.google.GoogleBackend',
         'social_auth.backends.yahoo.YahooBackend',
+        'social_auth.backends.browserid.BrowserIDBackend',
         'social_auth.backends.contrib.linkedin.LinkedinBackend',
         'social_auth.backends.contrib.livejournal.LiveJournalBackend',
         'social_auth.backends.contrib.orkut.OrkutBackend',
@@ -121,6 +120,7 @@ Configuration
         'social_auth.backends.contrib.github.GithubBackend',
         'social_auth.backends.contrib.dropbox.DropboxBackend',
         'social_auth.backends.contrib.flickr.FlickrBackend',
+        'social_auth.backends.contrib.instagram.InstagramBackend',
         'social_auth.backends.OpenIDBackend',
         'django.contrib.auth.backends.ModelBackend',
     )
@@ -159,6 +159,8 @@ Configuration
     DROPBOX_API_SECRET           = ''
     FLICKR_APP_ID                = ''
     FLICKR_API_SECRET            = ''
+    INSTAGRAM_CLIENT_ID          = ''
+    INSTAGRAM_CLIENT_SECRET      = ''
 
 - Setup login URLs::
 
@@ -213,10 +215,32 @@ Configuration
 
     TEMPLATE_CONTEXT_PROCESSORS = (
         ...
+        'social_auth.context_processors.social_auth_by_name_backends',
+        'social_auth.context_processors.social_auth_backends',
         'social_auth.context_processors.social_auth_by_type_backends',
     )
 
-   check `social_auth.context_processors`.
+  * ``social_auth_by_name_backends``:
+    Adds a ``social_auth`` dict where each key is a provider name and its value
+    is a UserSocialAuth instance if user has associated an account with that
+    provider, otherwise ``None``.
+
+  * ``social_auth_backends``:
+    Adds a ``social_auth`` dict with keys are ``associated``, ``not_associated`` and
+    ``backends``. ``associated`` key is a list of ``UserSocialAuth`` instances
+    associated with current user. ``not_associated`` is a list of providers names
+    that the current user doesn't have any association yet. ``backends`` holds
+    the list of backend names supported.
+
+  * ``social_auth_by_type_backends``:
+    Simiar to ``social_auth_backends`` but each value is grouped by backend type
+    ``openid``, ``oauth2`` and ``oauth``.
+
+  Check ``social_auth.context_processors`` for details.
+
+  **Note**:
+  ``social_auth_backends`` and ``social_auth_by_type_backends`` don't play nice
+  together.
 
 - Sync database to create needed models::
 
@@ -308,6 +332,12 @@ Configuration
 
       <uppercase backend name>_AUTH_EXTRA_ARGUMENTS = {...}
 
+- Also, you can send extra parameters on request token process by defining
+  settings per provider in the same way explained above but with this other
+  suffix::
+
+      <uppercase backend name>_REQUEST_TOKEN_EXTRA_ARGUMENTS = {...}
+
 - By default the application doesn't make redirects to different domains, to
   disable this behavior::
 
@@ -320,7 +350,135 @@ Configuration
 
   Defaults to ``LOGIN_ERROR_URL``.
 
--------------
+- The app catches any exception and logs errors to ``logger`` or
+  ``django.contrib.messagess`` app. Having tracebacks is really useful when
+  debugging, for that purpose this setting was defined::
+
+    SOCIAL_AUTH_RAISE_EXCEPTIONS = DEBUG
+
+  It's default value is ``DEBUG``, so you need to set it to ``False`` to avoid
+  tracebacks when ``DEBUG = True``.
+
+Authentication Pipeline
+-----------------------
+
+The final process of the authentication workflow is handled by a operations
+pipeline where custom functions can be added or default items can be removed to
+provide a custom behavior.
+
+The default pipeline mimics the user creation and basic data gathering from
+previous django-social-auth_ versions and a big set of settings (listed below)
+that were used to alter the default behavior are now deprecated in favor of
+pipeline overrides.
+
+The default pipeline is composed by::
+
+    (
+        'social_auth.backends.pipeline.social.social_auth_user',
+        'social_auth.backends.pipeline.associate.associate_by_email',
+        'social_auth.backends.pipeline.user.get_username',
+        'social_auth.backends.pipeline.user.create_user',
+        'social_auth.backends.pipeline.social.associate_user',
+        'social_auth.backends.pipeline.social.load_extra_data',
+        'social_auth.backends.pipeline.user.update_user_details'
+    )
+
+But it's possible to override it by defining the setting
+``SOCIAL_AUTH_PIPELINE``, for example a pipeline that won't create users, just
+accept already registered ones would look like this::
+
+    SOCIAL_AUTH_PIPELINE = (
+        'social_auth.backends.pipeline.social.social_auth_user',
+        'social_auth.backends.pipeline.social.load_extra_data',
+        'social_auth.backends.pipeline.user.update_user_details'
+    )
+
+Each pipeline function will receive the following parameters:
+    * Current social authentication backend
+    * User ID given by authentication provider
+    * User details given by authentication provider
+    * ``is_new`` flag (initialized in ``False``)
+    * Any arguments passed to ``auth_complete`` backend method, default views
+      pass this arguments:
+
+      - current logged in user (if it's logged in, otherwise ``None``)
+      - current request
+
+Each pipeline entry must return a ``dict`` or ``None``, any value in the
+``dict`` will be used in the ``kwargs`` argument for the next pipeline entry.
+
+The workflow will be cut if the exception ``social_auth.backends.exceptions.StopPipeline``
+is raised at any point.
+
+If any function returns something else beside a ``dict`` or ``None``, the
+workflow will be cut and the value returned immediately, this is useful to
+return ``HttpReponse`` instances like ``HttpResponseRedirect``.
+
+Partial Pipeline
+----------------
+
+It's possible to cut the pipeline process to return to the user asking for more
+data and resume the process later, to accomplish this add the entry
+``social_auth.backends.pipeline.misc.save_status_to_session`` (or a similar
+implementation) to the pipeline setting before any entry that returns an
+``HttpResponse`` instance::
+
+    SOCIAL_AUTH_PIPELINE = (
+        ...
+        social_auth.backends.pipeline.misc.save_status_to_session,
+        app.pipeline.redirect_to_basic_user_data_form
+        ...
+    )
+
+When it's time to resume the process just redirect the user to
+``/complete/<backend>/`` view. By default the pipeline will be resumed in the
+next entry after ``save_status_to_session`` but this can be modified by setting
+the following setting to the import path of the pipeline entry to resume
+processing::
+
+    SOCIAL_AUTH_PIPELINE_RESUME_ENTRY = 'social_auth.backends.pipeline.misc.save_status_to_session'
+
+``save_status_to_session`` saves needed data into user session, the key can be
+defined by ``SOCIAL_AUTH_PARTIAL_PIPELINE_KEY`` which default value is
+``partial_pipeline``::
+
+    SOCIAL_AUTH_PARTIAL_PIPELINE_KEY = 'partial_pipeline'
+
+Check the `example application`_ to check a basic usage.
+
+Deprecated bits
+---------------
+
+The following settings are deprecated in favor of pipeline functions.
+
+- These settings should be avoided and override ``get_username`` pipeline entry
+  with the desired behavior::
+
+    SOCIAL_AUTH_FORCE_RANDOM_USERNAME
+    SOCIAL_AUTH_DEFAULT_USERNAME
+    SOCIAL_AUTH_UUID_LENGTH
+    SOCIAL_AUTH_USERNAME_FIXER
+
+- User creation setting should be avoided and remove the entry ``create_user``
+  from pipeline instead::
+
+    SOCIAL_AUTH_CREATE_USERS
+
+- Automatic data update should be stopped by overriding ``update_user_details``
+  pipeline entry instead of using this setting::
+
+    SOCIAL_AUTH_CHANGE_SIGNAL_ONLY
+
+- Extra data retrieval from providers should be stopped by removing
+  ``load_extra_data`` from pipeline instead of using this setting::
+
+    SOCIAL_AUTH_EXTRA_DATA
+
+- Automatic email association should be avoided by removing
+  ``associate_by_email`` pipeline entry instead of using this setting::
+
+    SOCIAL_AUTH_ASSOCIATE_BY_MAIL
+
 Usage example
 -------------
 
@@ -344,9 +502,9 @@ In the example above we assume that Twitter and Facebook authentication backends
     FACEBOOK_APP_ID = 'real id here'
     FACEBOOK_API_SECRET = 'real secret here'
 
--------
 Signals
 -------
+
 A ``pre_update`` signal is sent when user data is about to be updated with new
 values from authorization service provider, this apply to new users and already
 existent ones. This is useful to update custom user fields or `User Profiles`_,
@@ -382,10 +540,12 @@ created::
 
     socialauth_registered.connect(new_users_handler, sender=None)
 
+Backends
+--------
 
-------
 OpenId
-------
+^^^^^^
+
 OpenId_ support is simpler to implement than OAuth_. Google and Yahoo
 providers are supported by default, others are supported by POST method
 providing endpoint URL.
@@ -409,10 +569,9 @@ Example::
 Settings must be a list of tuples mapping value name in response and value
 alias used to store.
 
-
------
 OAuth
------
+^^^^^
+
 OAuth_ communication demands a set of keys exchange to validate the client
 authenticity prior to user approbation. Twitter, Facebook and Orkut
 facilitates these keys by application registration, Google works the same,
@@ -435,10 +594,9 @@ Example::
 Settings must be a list of tuples mapping value name in response and value
 alias used to store.
 
-
--------
 Twitter
--------
+^^^^^^^
+
 Twitter offers per application keys named ``Consumer Key`` and ``Consumer Secret``.
 To enable Twitter these two keys are needed. Further documentation at
 `Twitter development resources`_:
@@ -456,10 +614,9 @@ To enable Twitter these two keys are needed. Further documentation at
   Client type instead of the Browser. Almost any dummy value will work if
   you plan some test.
 
-
---------
 Facebook
---------
+^^^^^^^^
+
 Facebook works similar to Twitter but it's simpler to setup and redirect URL
 is passed as a parameter when issuing an authorization. Further documentation
 at `Facebook development resources`_:
@@ -480,10 +637,9 @@ http://127.0.0.1:8000 or http://localhost:8000 because it won't work when
 testing. Instead I define http://myapp.com and setup a mapping on /etc/hosts
 or use dnsmasq_.
 
-
------
 Orkut
------
+^^^^^
+
 Orkut offers per application keys named ``Consumer Key`` and ``Consumer Secret``.
 To enable Orkut these two keys are needed.
 
@@ -503,10 +659,9 @@ your consumer_key and consumer_secret keys.
 
       ORKUT_EXTRA_SCOPES = [...]
 
-
-------------
 Google OAuth
-------------
+^^^^^^^^^^^^
+
 Google provides ``Consumer Key`` and ``Consumer Secret`` keys to registered
 applications, but also allows unregistered application to use their authorization
 system with, but beware that this method will display a security banner to the
@@ -535,10 +690,9 @@ anonymous values will be used if not configured as described in their
 
 Check which applications can be included in their `Google Data Protocol Directory`_
 
-
--------------
 Google OAuth2
--------------
+^^^^^^^^^^^^^
+
 Recently Google launched OAuth2 support following the definition at `OAuth2 draft`.
 It works in a similar way to plain OAuth mechanism, but developers **must** register
 an application and apply for a set of keys. Check `Google OAuth2`_ document for details.
@@ -564,10 +718,9 @@ To enable OAuth2 support:
 
 Check which applications can be included in their `Google Data Protocol Directory`_
 
-
---------
 LinkedIn
---------
+^^^^^^^^
+
 LinkedIn setup is similar to any other OAuth service. To request extra fields
 using `LinkedIn fields selectors`_ just define the setting::
 
@@ -578,27 +731,26 @@ way the values will be stored in ``UserSocialAuth.extra_data`` field.
 
 By default ``id``, ``first-name`` and ``last-name`` are requested and stored.
 
-
-------
 GitHub
-------
+^^^^^^
+
 GitHub works similar to Facebook (OAuth).
 
-- Register a new application at `GitHub Developers`_, and
+- Register a new application at `GitHub Developers`_, set your site domain as
+  the callback URL or it might cause some troubles when associating accounts,
 
-- fill ``App Id`` and ``App Secret`` values in the settings::
+- Fill ``App Id`` and ``App Secret`` values in the settings::
 
       GITHUB_APP_ID = ''
       GITHUB_API_SECRET = ''
 
-- also it's possible to define extra permissions with::
+- Also it's possible to define extra permissions with::
 
      GITHUB_EXTENDED_PERMISSIONS = [...]
 
-
--------
 Dropbox
--------
+^^^^^^^
+
 Dropbox uses OAuth v1.0 for authentication.
 
 - Register a new application at `Dropbox Developers`_, and
@@ -608,10 +760,9 @@ Dropbox uses OAuth v1.0 for authentication.
       DROPBOX_APP_ID = ''
       DROPBOX_API_SECRET = ''
 
-
-------
 Flickr
-------
+^^^^^^
+
 Flickr uses OAuth v1.0 for authentication.
 
 - Register a new application at the `Flickr App Garden`_, and
@@ -621,10 +772,40 @@ Flickr uses OAuth v1.0 for authentication.
       FLICKR_APP_ID = ''
       FLICKR_API_SECRET = ''
 
+BrowserID
+^^^^^^^^^
+
+Support for BrowserID_ is possible by posting the ``assertion`` code to
+``/complete/browserid/`` URL.
+
+The setup doesn't need any setting, just the usual BrowserID_ javascript
+include in your document and the needed mechanism to trigger the POST to
+`django-social-auth`_.
+
+Check the second "Use Case" for an implementation example.
+
+Instagram
+^^^^^^^^^
+
+Instagram uses OAuth v2 for Authentication
+
+- Register a new application at the `Instagram API`_, and
+
+- fill ``Client Id`` and ``Client Secret`` values in the settings::
+
+      INSTAGRAM_CLIENT_ID = ''
+      INSTAGRAM_CLIENT_SECRET = ''
+
+.. note::
+
+    Instagram only allows one callback url so you'll have to change your urls.py to
+    accomodate both ``/complete`` and ``/associate`` routes, for example by having
+    a single ``/associate`` url which takes a ``?complete=true`` parameter for the
+    cases when you want to complete rather than associate.
 
--------
 Testing
 -------
+
 To test the app just run::
 
     ./manage.py test social_auth
@@ -659,36 +840,81 @@ fill the needed account information. Then run::
     cd contrib/tests
     ./runtests.py
 
+Use Cases
+---------
+Some particular use cases are listed below.
+
+1. Use social auth just for account association (no login)::
+
+    urlpatterns += patterns('',
+        url(r'^associate/(?P<backend>[^/]+)/$', associate,
+            name='socialauth_associate_begin'),
+        url(r'^associate/complete/(?P<backend>[^/]+)/$', associate_complete,
+            name='socialauth_associate_complete'),
+        url(r'^disconnect/(?P<backend>[^/]+)/$', disconnect,
+            name='socialauth_disconnect'),
+        url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$',
+            disconnect, name='socialauth_disconnect_individual'),
+    )
+
+2. Include a similar snippet in your page to make BrowserID_ work::
+
+    <!-- Include BrowserID JavaScript -->
+    <script src="https://browserid.org/include.js" type="text/javascript"></script>
+
+    <!-- Define a form to send the POST data -->
+    <form method="post" action="{% url socialauth_complete "browserid" %}">
+        <input type="hidden" name="assertion" value="" />
+        <a rel="nofollow" id="browserid" href="#">BrowserID</a>
+    </form>
+
+    <!-- Setup click handler that retieves BrowserID assertion code and sends
+         POST data -->
+    <script type="text/javascript">
+        $(function () {
+            $('#browserid').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>
 
--------------
 Miscellaneous
 -------------
 
 Join to django-social-auth_ community on Convore_ and bring any questions or
 suggestions that will improve this app.
 
-
 If defining a custom user model, do not import social_auth from any models.py
 that would finally import from the models.py that defines your User class or it
 will make your project fail with a recursive import because social_auth uses
 get_model() to retrieve your User.
 
-
 There's an ongoing movement to create a list of third party backends on
 djangopackages.com_, so, if somebody doesn't want it's backend in the
 ``contrib`` directory but still wants to share, just split it in a separated
 package and link it there.
 
-
-----
 Bugs
 ----
-Maybe several, please create `issues in github`_
 
+Maybe several, please create `issues in github`_
 
-------------
 Contributors
 ------------
+
 Attributions to whom deserves:
 
 - caioariede_ (Caio Ariede):
@@ -733,9 +959,13 @@ Attributions to whom deserves:
   - Flickr support
   - Provider name context processor
 
-----------
+- r4vi_ (Ravi Kotecha)
+
+  - Instagram support
+
 Copyrights
 ----------
+
 Base work is copyrighted by:
 
 - django-twitter-oauth::
@@ -795,9 +1025,10 @@ Base work is copyrighted by:
 .. _mattucf: https://github.com/mattucf
 .. _Quard: https://github.com/Quard
 .. _micrypt: https://github.com/micrypt
+.. _r4vi: https://github.com/r4vi
 .. _South: http://south.aeracode.org/
 .. _bedspax: https://github.com/bedspax
-.. _django-social-auth: https://convore.com/django-social-auth/
+.. _django-social-auth: https://github.com/omab/django-social-auth
 .. _Convore: https://convore.com/
 .. _Selenium: http://seleniumhq.org/
 .. _LinkedIn fields selectors: http://developer.linkedin.com/docs/DOC-1014
@@ -811,3 +1042,6 @@ Base work is copyrighted by:
 .. _Flickr OAuth: http://www.flickr.com/services/api/
 .. _Flickr App Garden: http://www.flickr.com/services/apps/create/
 .. _danielgtaylor: https://github.com/danielgtaylor
+.. _example application: https://github.com/omab/django-social-auth/blob/master/example/local_settings.py.template#L23
+.. _BrowserID: https://browserid.org
+.. _Instagram API: http://instagr.am/developer/
diff --git a/doc/backends/browserid.rst b/doc/backends/browserid.rst
new file mode 100644 (file)
index 0000000..021ad48
--- /dev/null
@@ -0,0 +1,14 @@
+---------
+BrowserID
+---------
+Support for BrowserID_ is possible by posting the ``assertion`` code to
+``/complete/browserid/`` URL.
+
+The setup doesn't need any setting, just the usual BrowserID_ javascript
+include in your document and the needed mechanism to trigger the POST to
+`django-social-auth`_.
+
+Check the second "Use Case" for an implementation example.
+
+.. _django-social-auth: https://github.com/omab/django-social-auth
+.. _BrowserID: https://browserid.org
index ea46e469df957eb473e8b67a409c7cc397688aef..293542bdaf68a0b44bc7b8d71747a4f52a6e2681 100644 (file)
@@ -2,16 +2,16 @@ GitHub
 ======
 Github works similar to Facebook (OAuth).
 
-- Register a new application at `GitHub Developers`_, and
+- Register a new application at `GitHub Developers`_, set your site domain as
+  the callback URL or it might cause some troubles when associating accounts,
 
-- fill ``App Id`` and ``App Secret`` values in the settings::
+- Fill ``App Id`` and ``App Secret`` values in the settings::
 
       GITHUB_APP_ID = ''
       GITHUB_API_SECRET = ''
 
-- also it's possible to define extra permissions with::
+- Also it's possible to define extra permissions with::
 
      GITHUB_EXTENDED_PERMISSIONS = [...]
 
-
 .. _GitHub Developers: https://github.com/account/applications/new
index c3cbcfdcca0fbcec1a0268f3b751fe954aed825c..c73cc7eb40b6b0dc59717df5835f9d8396233ed2 100644 (file)
@@ -65,6 +65,16 @@ To enable OAuth2 support:
 Check which applications can be included in their `Google Data Protocol Directory`_
 
 
+Google OpenID 
+-------------
+
+Configurable settings:
+
+- Supply a list of domain strings to be checked. The default (empty list) allows all domains.  If a list is provided and a user attempts to sign in with a Google account that is not in the list, then a ValueError will be raised and the user will be redirected to your login error page::
+
+    GOOGLE_WHITE_LISTED_DOMAINS = ['mydomain.com']
+
+
 Orkut
 -----
 
index 4cb8465e8642cd34c9beb0b6cac65fff8731a71b..d4dba774463327c11cbbeb405262ebdb824d4c5d 100644 (file)
@@ -13,3 +13,5 @@ Contents:
    facebook
    linkedin
    github
+   browserid
+   instagram
diff --git a/doc/backends/instagram.rst b/doc/backends/instagram.rst
new file mode 100644 (file)
index 0000000..2bc4260
--- /dev/null
@@ -0,0 +1,18 @@
+Instagram
+=========
+Instagram uses OAuth v2 for Authentication
+
+- Register a new application at the `Instagram API`_, and
+
+- fill ``Client Id`` and ``Client Secret`` values in the settings::
+
+      INSTAGRAM_CLIENT_ID = ''
+      INSTAGRAM_CLIENT_SECRET = ''
+
+*Note:*
+Instagram only allows one callback url so you'll have to change your urls.py to
+accomodate both ``/complete`` and ``/associate`` routes, for example by having
+a single ``/associate`` url which takes a ``?complete=true`` parameter for the
+cases when you want to complete rather than associate.
+
+.. _Instagram API: http://instagr.am/developer/
index f62a3a8fdae8115f1b64b203b63e51fdf4a6d1da..3b016201bcfc6fb6850ce0aef00fd1d8d6adae14 100644 (file)
@@ -17,6 +17,7 @@ Configuration
         'social_auth.backends.google.GoogleOAuth2Backend',
         'social_auth.backends.google.GoogleBackend',
         'social_auth.backends.yahoo.YahooBackend',
+        'social_auth.backends.browserid.BrowserIDBackend',
         'social_auth.backends.contrib.linkedin.LinkedinBackend',
         'social_auth.backends.contrib.livejournal.LiveJournalBackend',
         'social_auth.backends.contrib.orkut.OrkutBackend',
@@ -108,10 +109,32 @@ Configuration
 
     TEMPLATE_CONTEXT_PROCESSORS = (
         ...
+        'social_auth.context_processors.social_auth_by_name_backends',
+        'social_auth.context_processors.social_auth_backends',
         'social_auth.context_processors.social_auth_by_type_backends',
     )
 
-   check `social_auth.context_processors`.
+  * ``social_auth_by_name_backends``:
+    Adds a ``social_auth`` dict where each key is a provider name and its value
+    is a UserSocialAuth instance if user has associated an account with that
+    provider, otherwise ``None``.
+
+  * ``social_auth_backends``:
+    Adds a ``social_auth`` dict with keys are ``associated``, ``not_associated`` and
+    ``backends``. ``associated`` key is a list of ``UserSocialAuth`` instances
+    associated with current user. ``not_associated`` is a list of providers names
+    that the current user doesn't have any association yet. ``backends`` holds
+    the list of backend names supported.
+
+  * ``social_auth_by_type_backends``:
+    Simiar to ``social_auth_backends`` but each value is grouped by backend type
+    ``openid``, ``oauth2`` and ``oauth``.
+
+  Check ``social_auth.context_processors`` for details.
+
+  **Note**:
+  ``social_auth_backends`` and ``social_auth_by_type_backends`` don't play nice
+  together.
 
 - Sync database to create needed models::
 
@@ -202,6 +225,12 @@ Configuration
 
       <uppercase backend name>_AUTH_EXTRA_ARGUMENTS = {...}
 
+- Also, you can send extra parameters on request token process by defining
+  settings per provider in the same way explained above but with this other
+  suffix::
+
+      <uppercase backend name>_REQUEST_TOKEN_EXTRA_ARGUMENTS = {...}
+
 - By default the application doesn't make redirects to different domains, to
   disable this behavior::
 
@@ -214,6 +243,14 @@ Configuration
 
   Defaults to ``LOGIN_ERROR_URL``.
 
+- The app catches any exception and logs errors to ``logger`` or
+  ``django.contrib.messagess`` app. Having tracebacks is really useful when
+  debugging, for that purpose this setting was defined::
+
+    SOCIAL_AUTH_RAISE_EXCEPTIONS = DEBUG
+
+  It's default value is ``DEBUG``, so you need to set it to ``False`` to avoid
+  tracebacks when ``DEBUG = True``.
 
 .. _Model Manager: http://docs.djangoproject.com/en/dev/topics/db/managers/#managers
 .. _Login URL: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#login-url
diff --git a/doc/deprecated.rst b/doc/deprecated.rst
new file mode 100644 (file)
index 0000000..60d3ff6
--- /dev/null
@@ -0,0 +1,32 @@
+Deprecated bits
+===============
+
+The following settings are deprecated in favor of pipeline functions.
+
+- These settings should be avoided and override ``get_username`` pipeline entry
+  with the desired behavior::
+
+    SOCIAL_AUTH_FORCE_RANDOM_USERNAME
+    SOCIAL_AUTH_DEFAULT_USERNAME
+    SOCIAL_AUTH_UUID_LENGTH
+    SOCIAL_AUTH_USERNAME_FIXER
+
+- User creation setting should be avoided and remove the entry ``create_user``
+  from pipeline instead::
+
+    SOCIAL_AUTH_CREATE_USERS
+
+- Automatic data update should be stopped by overriding ``update_user_details``
+  pipeline entry instead of using this setting::
+
+    SOCIAL_AUTH_CHANGE_SIGNAL_ONLY
+
+- Extra data retrieval from providers should be stopped by removing
+  ``load_extra_data`` from pipeline instead of using this setting::
+
+    SOCIAL_AUTH_EXTRA_DATA
+
+- Automatic email association should be avoided by removing
+  ``associate_by_email`` pipeline entry instead of using this setting::
+
+    SOCIAL_AUTH_ASSOCIATE_BY_MAIL
index 926317cd0bb47414212f4fc1624eb1b819457adf..d9b711ee7c2da73d348d11720dcab604b0fd3c30 100644 (file)
@@ -15,10 +15,13 @@ Contents:
    configuration
 
    backends/index
+   pipeline
+   deprecated
 
    signals
    contributions
    testing
+   use_cases
    miscellaneous
    bugs
 
index ea32f25a9e20e98409396066c40ea3c92b6a7732..6d74dea918f9310e80f32dae1e353a38a84b8966 100644 (file)
@@ -39,6 +39,8 @@ credentials, some features are:
 
 - Custom User model override if needed (`auth.User`_ by default)
 
+- Extensible pipeline to handle authentication/association mechanism
+
 .. _auth.User: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/models.py#L186
 .. _OpenId: http://openid.net/
 .. _OAuth: http://oauth.net/
index 410e69f63c5d492ba21fbf74f51a8876c9ec23a7..1c1b4ee5293d8d4ac6d271262273e281e79095e8 100644 (file)
@@ -26,6 +26,7 @@ package and link it there.
 
 
 .. _South: http://south.aeracode.org/
-.. _django-social-auth: https://convore.com/django-social-auth/
+.. _django-social-auth: https://github.com/omab/django-social-auth
 .. _Convore: https://convore.com/
 .. _djangopackages.com: http://djangopackages.com/grids/g/social-auth-backends/
+
diff --git a/doc/pipeline.rst b/doc/pipeline.rst
new file mode 100644 (file)
index 0000000..776e5d5
--- /dev/null
@@ -0,0 +1,90 @@
+Authentication Pipeline
+=======================
+
+The final process of the authentication workflow is handled by a operations
+pipeline where custom functions can be added or default items can be removed to
+provide a custom behavior.
+
+The default pipeline mimics the user creation and basic data gathering from
+previous django-social-auth_ versions and a big set of settings (listed below)
+that were used to alter the default behavior are now deprecated in favor of
+pipeline overrides.
+
+The default pipeline is composed by::
+
+    (
+        'social_auth.backends.pipeline.social.social_auth_user',
+        'social_auth.backends.pipeline.associate.associate_by_email',
+        'social_auth.backends.pipeline.user.get_username',
+        'social_auth.backends.pipeline.user.create_user',
+        'social_auth.backends.pipeline.social.associate_user',
+        'social_auth.backends.pipeline.social.load_extra_data',
+        'social_auth.backends.pipeline.user.update_user_details'
+    )
+
+But it's possible to override it by defining the setting
+``SOCIAL_AUTH_PIPELINE``, for example a pipeline that won't create users, just
+accept already registered ones would look like this::
+
+    SOCIAL_AUTH_PIPELINE = (
+        'social_auth.backends.pipeline.social.social_auth_user',
+        'social_auth.backends.pipeline.social.load_extra_data',
+        'social_auth.backends.pipeline.user.update_user_details'
+    )
+
+Each pipeline function will receive the following parameters:
+    * Current social authentication backend
+    * User ID given by authentication provider
+    * User details given by authentication provider
+    * ``is_new`` flag (initialized in ``False``)
+    * Any arguments passed to ``auth_complete`` backend method, default views
+      pass this arguments:
+        - current logged in user (if it's logged in, otherwise ``None``)
+        - current request
+
+Each pipeline entry must return a ``dict`` or ``None``, any value in the
+``dict`` will be used in the ``kwargs`` argument for the next pipeline entry.
+
+The workflow will be cut if the exception ``social_auth.backends.exceptions.StopPipeline``
+is raised at any point.
+
+If any function returns something else beside a ``dict`` or ``None``, the
+workflow will be cut and the value returned immediately, this is useful to
+return ``HttpReponse`` instances like ``HttpResponseRedirect``.
+
+
+Partial Pipeline
+----------------
+
+It's possible to cut the pipeline process to return to the user asking for more
+data and resume the process later, to accomplish this add the entry
+``social_auth.backends.pipeline.misc.save_status_to_session`` (or a similar
+implementation) to the pipeline setting before any entry that returns an
+``HttpResponse`` instance::
+
+    SOCIAL_AUTH_PIPELINE = (
+        ...
+        social_auth.backends.pipeline.misc.save_status_to_session,
+        app.pipeline.redirect_to_basic_user_data_form
+        ...
+    )
+
+When it's time to resume the process just redirect the user to
+``/complete/<backend>/`` view. By default the pipeline will be resumed in the
+next entry after ``save_status_to_session`` but this can be modified by setting
+the following setting to the import path of the pipeline entry to resume
+processing::
+
+    SOCIAL_AUTH_PIPELINE_RESUME_ENTRY = 'social_auth.backends.pipeline.misc.save_status_to_session'
+
+``save_status_to_session`` saves needed data into user session, the key can be
+defined by ``SOCIAL_AUTH_PARTIAL_PIPELINE_KEY`` which default value is
+``partial_pipeline``::
+
+    SOCIAL_AUTH_PARTIAL_PIPELINE_KEY = 'partial_pipeline'
+
+Check the `example application`_ to check a basic usage.
+
+
+.. _django-social-auth: https://github.com/omab/django-social-auth
+.. _example application: https://github.com/omab/django-social-auth/blob/master/example/local_settings.py.template#L23
diff --git a/doc/use_cases.rst b/doc/use_cases.rst
new file mode 100644 (file)
index 0000000..2934b86
--- /dev/null
@@ -0,0 +1,50 @@
+Use Cases
+=========
+
+Some particular use cases are listed below.
+
+1. Use social auth just for account association (no login)::
+
+    urlpatterns += patterns('',
+        url(r'^associate/(?P<backend>[^/]+)/$', associate,
+            name='socialauth_associate_begin'),
+        url(r'^associate/complete/(?P<backend>[^/]+)/$', associate_complete,
+            name='socialauth_associate_complete'),
+        url(r'^disconnect/(?P<backend>[^/]+)/$', disconnect,
+            name='socialauth_disconnect'),
+        url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$',
+            disconnect, name='socialauth_disconnect_individual'),
+    )
+
+2. Include a similar snippet in your page to make BrowserID_ work::
+    <!-- Include BrowserID JavaScript -->
+    <script src="https://browserid.org/include.js" type="text/javascript"></script>
+
+    <!-- Define a form to send the POST data -->
+    <form method="post" action="{% url socialauth_complete "browserid" %}">
+        <input type="hidden" name="assertion" value="" />
+        <a rel="nofollow" id="browserid" href="#">BrowserID</a>
+    </form>
+
+    <!-- Setup click handler that retieves BrowserID assertion code and sends
+         POST data -->
+    <script type="text/javascript">
+        $(function () {
+            $('#browserid').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>
diff --git a/example/app/pipeline.py b/example/app/pipeline.py
new file mode 100644 (file)
index 0000000..0d81b17
--- /dev/null
@@ -0,0 +1,14 @@
+from django.http import HttpResponseRedirect
+
+
+def username(request, *args, **kwargs):
+    if kwargs.get('user'):
+        username = kwargs['user'].username
+    else:
+        username = request.session.get('saved_username')
+    return { 'username': username }
+
+
+def redirect_to_form(*args, **kwargs):
+    if not kwargs['request'].session.get('saved_username') and kwargs.get('user') is None:
+        return HttpResponseRedirect('/form/')
index 8c4eaef32b93db6f655b985b1cb707575f9bcda4..4a4c91ed2f2c890419aa6ea04ba9e3ae784af354 100644 (file)
@@ -2,10 +2,11 @@ from django.http import HttpResponseRedirect
 from django.contrib.auth import logout as auth_logout
 from django.contrib.auth.decorators import login_required
 from django.template import RequestContext
-from django.shortcuts import render_to_response
+from django.shortcuts import render_to_response, redirect
 from django.contrib.messages.api import get_messages
 
 from social_auth import __version__ as version
+from social_auth.utils import setting
 
 def home(request):
     """Home view, displays login mechanism"""
@@ -18,8 +19,10 @@ def home(request):
 @login_required
 def done(request):
     """Login complete view, displays user data"""
-    ctx = {'version': version,
-           'last_login': request.session.get('social_auth_last_login_backend')}
+    ctx = {
+        'version': version,
+        'last_login': request.session.get('social_auth_last_login_backend')
+    }
     return render_to_response('done.html', ctx, RequestContext(request))
 
 def error(request):
@@ -33,3 +36,12 @@ def logout(request):
     """Logs out user"""
     auth_logout(request)
     return HttpResponseRedirect('/')
+
+
+def form(request):
+    if request.method == 'POST' and request.POST.get('username'):
+        name = setting('SOCIAL_AUTH_PARTIAL_PIPELINE_KEY', 'partial_pipeline')
+        request.session['saved_username'] = request.POST['username']
+        backend = request.session[name]['backend']
+        return redirect('socialauth_complete', backend=backend)
+    return render_to_response('form.html', {}, RequestContext(request))
index 7e928f14880523421d9af34958424cf6016d2d01..bfbb541656643ed2e0768bffa2c7064a182c1f15 100644 (file)
@@ -33,4 +33,16 @@ FOURSQUARE_CONSUMER_KEY           = ''
 FOURSQUARE_CONSUMER_SECRET        = ''
 YANDEX_OAUTH2_CLIENT_KEY          = ''
 YANDEX_OAUTH2_CLIENT_SECRET       = ''
-YANDEX_OAUTH2_API_URL             = 'https://api-yaru.yandex.ru/me/' # http://api.moikrug.ru/v1/my/ for Moi Krug
\ No newline at end of file
+YANDEX_OAUTH2_API_URL             = 'https://api-yaru.yandex.ru/me/' # http://api.moikrug.ru/v1/my/ for Moi Krug
+
+SOCIAL_AUTH_PIPELINE = (
+    'social_auth.backends.pipeline.social.social_auth_user',
+    'social_auth.backends.pipeline.associate.associate_by_email',
+    'social_auth.backends.pipeline.misc.save_status_to_session',
+    'app.pipeline.redirect_to_form',
+    'app.pipeline.username',
+    'social_auth.backends.pipeline.user.create_user',
+    'social_auth.backends.pipeline.social.associate_user',
+    'social_auth.backends.pipeline.social.load_extra_data',
+    'social_auth.backends.pipeline.user.update_user_details',
+)
\ No newline at end of file
index 797e7df000271e6dfca70d80eb413285766109af..a6a26ac3439210a1c08e679bdcd247925c70d5c5 100644 (file)
@@ -76,8 +76,10 @@ AUTHENTICATION_BACKENDS = (
     'social_auth.backends.yahoo.YahooBackend',
     'social_auth.backends.contrib.linkedin.LinkedinBackend',
     'social_auth.backends.contrib.flickr.FlickrBackend',
+    'social_auth.backends.contrib.instagram.InstagramBackend',
     'social_auth.backends.OpenIDBackend',
     'social_auth.backends.contrib.livejournal.LiveJournalBackend',
+    'social_auth.backends.browserid.BrowserIDBackend',
     'social_auth.backends.contrib.vkontakte.VKontakteBackend',
     'social_auth.backends.contrib.yandex.YandexBackend',
     'social_auth.backends.contrib.yandex.YandexOAuth2Backend',
index b528e2fd37a571fb6ad83fed5f4ae9869bbdf1c2..9e1fd712e23380c479904a96ccc43a3856045ec3 100644 (file)
@@ -30,6 +30,8 @@
       #valid-badges {position: fixed; right: 10px; bottom: 10px;}
       #valid-badges p {display: inline;}
     </style>
+
+    {% block script %}{% endblock %}
   </head>
   <body>
     <h1>Django Social Auth (v{{ version }})</h1>
index 04b7b949c2adb9547866be9827f5971aeeb1e96b..edb64c601e2065b06910467253096fd8f0468881 100644 (file)
@@ -1,5 +1,10 @@
 {% extends "base.html" %}
 
+{% block script %}
+<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
+<script src="https://browserid.org/include.js" type="text/javascript"></script>
+{% endblock %}
+
 {% block heading %}Logged in!{% endblock %}
 
 {% block content %}
     </li>
   {% endfor %}
   </ul>
+
+  <h3>Associate new <a href="https://browserid.org/" title="BrowserID">BrowserID</a>:</h3>
+  <form method="post" action="{% url socialauth_complete "browserid" %}">
+    <input type="hidden" name="assertion" value="" />
+    <a rel="nofollow" id="browserid" href="#">BrowserID</a>
+    <script type="text/javascript">
+      $(function () {
+        $('#browserid').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>
+  </form>
 </div>
 
 <div>
diff --git a/example/templates/form.html b/example/templates/form.html
new file mode 100644 (file)
index 0000000..fc6e814
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block heading %}User basic form{% endblock %}
+
+{% block content %}
+<form action="" method="post">
+    {% csrf_token %}
+    <p>
+        <label for="id_username">Username</label>
+        <input type="text" name="username" id="id_username" value="" />
+    </p>
+
+    <input type="submit" value="Send" />
+</form>
+{% endblock %}
index c1abd7a3a09944431b782691a9e47021f59c8eca..edf90f888ec7bc8c5b42ad463c83dd67b9a3fbf7 100644 (file)
@@ -1,5 +1,10 @@
 {% extends "base.html" %}
 
+{% block script %}
+<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
+<script src="https://browserid.org/include.js" type="text/javascript"></script>
+{% endblock %}
+
 {% block heading %}Login using any of the following methods{% endblock %}
 
 {% block content %}
   </ul>
 </div>
 
+<div>
+  <h3>Login using <a href="https://browserid.org/" title="BrowserID">BrowserID</a>:</h3>
+  <form method="post" action="{% url socialauth_complete "browserid" %}">
+    <input type="hidden" name="assertion" value="" />
+    <a rel="nofollow" id="browserid" href="#">BrowserID</a>
+    <script type="text/javascript">
+      $(function () {
+        $('#browserid').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>
+  </form>
+</div>
+
 <div>
   <h3>Login using other authentication systems:</h3>
        <ul>
index bcede4173ae7d1d6c63f18fb46a373658135f6e6..8344d27d21de525b18131312233d0e121b913b03 100644 (file)
@@ -1,7 +1,7 @@
 from django.conf.urls.defaults import patterns, url, include
 from django.contrib import admin
 
-from app.views import home, done, logout, error
+from app.views import home, done, logout, error, form
 
 
 admin.autodiscover()
@@ -11,6 +11,7 @@ urlpatterns = patterns('',
     url(r'^done/$', done, name='done'),
     url(r'^error/$', error, name='error'),
     url(r'^logout/$', logout, name='logout'),
+    url(r'^form/$', form, name='form'),
     url(r'^admin/', include(admin.site.urls)),
     url(r'', include('social_auth.urls')),
 )
index 7716d9da688f24c757336fe70aad9246105617c4..40c9af73cc783b8105786ad2db92373ef17479e2 100644 (file)
@@ -2,5 +2,5 @@
 Django-social-auth application, allows OpenId or OAuth user
 registration/authentication just adding a few configurations.
 """
-version = (0, 6, 1)
+version = (0, 6, 5)
 __version__ = '.'.join(map(str, version))
index 6a287692529f6fd490e812f18d9987fd4339bb16..30e84efb997e3e2367b0173fd3f7a0727077b047 100644 (file)
@@ -9,9 +9,6 @@ Also the modules *must* define a BACKENDS dictionary with the backend name
 (which is used for URLs matching) and Auth class, otherwise it won't be
 enabled.
 """
-import logging
-logger = logging.getLogger(__name__)
-
 from urllib2 import Request, urlopen
 from urllib import urlencode
 from urlparse import urlsplit
@@ -24,19 +21,18 @@ from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest, \
                    SignatureMethod_HMAC_SHA1
 
 from django.db import models
-from django.conf import settings
 from django.contrib.auth import authenticate
 from django.contrib.auth.backends import ModelBackend
 from django.utils import simplejson
 from django.utils.importlib import import_module
 
-from social_auth.utils import setting
+from social_auth.utils import setting, log, model_to_ctype, ctype_to_model
 from social_auth.store import DjangoOpenIDStore
 from social_auth.backends.exceptions import StopPipeline
 
 
-if getattr(settings, 'SOCIAL_AUTH_USER_MODEL', None):
-    User = models.get_model(*settings.SOCIAL_AUTH_USER_MODEL.rsplit('.', 1))
+if setting('SOCIAL_AUTH_USER_MODEL'):
+    User = models.get_model(*setting('SOCIAL_AUTH_USER_MODEL').rsplit('.', 1))
 else:
     from django.contrib.auth.models import User
 
@@ -99,11 +95,18 @@ class SocialAuthBackend(ModelBackend):
             return None
 
         response = kwargs.get('response')
-        details = self.get_user_details(response)
-        uid = self.get_user_id(details, response)
-        out = self.pipeline(PIPELINE, backend=self, uid=uid,
-                            social_user=None, details=details,
-                            is_new=False, *args, **kwargs)
+        pipeline = PIPELINE
+        kwargs = kwargs.copy()
+        kwargs['backend'] = self
+
+        if 'pipeline_index' in kwargs:
+            pipeline = pipeline[kwargs['pipeline_index']:]
+        else:
+            kwargs['details'] = self.get_user_details(response)
+            kwargs['uid'] = self.get_user_id(kwargs['details'], response)
+            kwargs['is_new'] = False
+
+        out = self.pipeline(pipeline, *args, **kwargs)
         if not isinstance(out, dict):
             return out
 
@@ -125,7 +128,7 @@ class SocialAuthBackend(ModelBackend):
             try:
                 mod = import_module(mod_name)
             except ImportError:
-                logger.exception('Error importing pipeline %s', name)
+                log('exception', 'Error importing pipeline %s', name)
             else:
                 func = getattr(mod, func_name, None)
 
@@ -143,7 +146,7 @@ class SocialAuthBackend(ModelBackend):
 
     def extra_data(self, user, uid, response, details):
         """Return default blank user extra data"""
-        return ''
+        return {}
 
     def get_user_id(self, details, response):
         """Must return a unique ID from values returned on details"""
@@ -304,12 +307,51 @@ class BaseAuth(object):
         """Completes loging process, must return user instance"""
         raise NotImplementedError('Implement in subclass')
 
+    def to_session_dict(self, next_idx, *args, **kwargs):
+        """Returns dict to store on session for partial pipeline."""
+        return {
+            'next': next_idx,
+            'backend': self.AUTH_BACKEND.name,
+            'args': tuple(map(model_to_ctype, args)),
+            'kwargs': dict((key, model_to_ctype(val))
+                                for key, val in kwargs.iteritems())
+        }
+
+    def from_session_dict(self, entry, *args, **kwargs):
+        """Takes session saved entry to continue pipeline and merges with
+        any new extra argument needed. Returns tuple with next pipeline
+        index entry, arguments and keyword arguments to continue the
+        process."""
+        args = args[:] + tuple(map(ctype_to_model, entry['args']))
+
+        kwargs = kwargs.copy()
+        kwargs.update((key, ctype_to_model(val))
+                            for key, val in entry['kwargs'].iteritems())
+        return (entry['next'], args, kwargs)
+
+    def continue_pipeline(self, *args, **kwargs):
+        """Continue previous halted pipeline"""
+        kwargs.update({
+            'auth': self,
+            self.AUTH_BACKEND.name: True
+        })
+        return authenticate(*args, **kwargs)
+
+    def request_token_extra_arguments(self):
+        """Return extra arguments needed on request-token process,
+        setting is per backend and defined by:
+            <backend name in uppercase>_REQUEST_TOKEN_EXTRA_ARGUMENTS.
+        """
+        backend_name = self.AUTH_BACKEND.name.upper().replace('-','_')
+        return setting(backend_name + '_REQUEST_TOKEN_EXTRA_ARGUMENTS', {})
+
     def auth_extra_arguments(self):
-        """Return extra argumens needed on auth process, setting is per bancked
-        and defined by <backend name in uppercase>_AUTH_EXTRA_ARGUMENTS.
+        """Return extra arguments needed on auth process, setting is per
+        backend and defined by:
+            <backend name in uppercase>_AUTH_EXTRA_ARGUMENTS.
         """
-        name = self.AUTH_BACKEND.name.upper().replace('-','_') + '_AUTH_EXTRA_ARGUMENTS'
-        return getattr(settings, name, {})
+        backend_name = self.AUTH_BACKEND.name.upper().replace('-','_')
+        return setting(backend_name + '_AUTH_EXTRA_ARGUMENTS', {})
 
     @property
     def uses_redirect(self):
@@ -356,6 +398,17 @@ class OpenIdAuth(BaseAuth):
         return setting('OPENID_TRUST_ROOT') or \
                self.request.build_absolute_uri('/')
 
+    def continue_pipeline(self, *args, **kwargs):
+        """Continue previous halted pipeline"""
+        response = self.consumer().complete(dict(self.data.items()),
+                                            self.request.build_absolute_uri())
+        kwargs.update({
+            'auth': self,
+            'response': response,
+            self.AUTH_BACKEND.name: True
+        })
+        return authenticate(*args, **kwargs)
+
     def auth_complete(self, *args, **kwargs):
         """Complete auth process"""
         response = self.consumer().complete(dict(self.data.items()),
@@ -363,7 +416,11 @@ class OpenIdAuth(BaseAuth):
         if not response:
             raise ValueError('This is an OpenID relying party endpoint')
         elif response.status == SUCCESS:
-            kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
+            kwargs.update({
+                'auth': self,
+                'response': response,
+                self.AUTH_BACKEND.name: True
+            })
             return authenticate(*args, **kwargs)
         elif response.status == FAILURE:
             raise ValueError('OpenID authentication failed: %s' % \
@@ -471,19 +528,26 @@ class ConsumerBasedOAuth(BaseOAuth):
         if data is not None:
             data['access_token'] = access_token.to_string()
 
-        kwargs.update({'response': data, self.AUTH_BACKEND.name: True})
+        kwargs.update({
+            'auth': self,
+            'response': data,
+            self.AUTH_BACKEND.name: True
+        })
         return authenticate(*args, **kwargs)
 
     def unauthorized_token(self):
         """Return request for unauthorized token (first stage)"""
-        request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL)
+        request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL,
+                             extra_params=self.request_token_extra_arguments())
         response = self.fetch_response(request)
         return Token.from_string(response)
 
     def oauth_authorization_request(self, token):
         """Generate OAuth request to authorize token."""
-        return self.oauth_request(token, self.AUTHORIZATION_URL,
-                                  self.auth_extra_arguments())
+        return OAuthRequest.from_token_and_callback(token=token,
+                                        callback=self.redirect_uri,
+                                        http_url=self.AUTHORIZATION_URL,
+                                        parameters=self.auth_extra_arguments())
 
     def oauth_request(self, token, url, extra_params=None):
         """Generate OAuth request, setups callback url"""
@@ -529,8 +593,8 @@ class ConsumerBasedOAuth(BaseOAuth):
     @classmethod
     def enabled(cls):
         """Return backend enabled status by checking basic settings"""
-        return all(hasattr(settings, name) for name in
-                        (cls.SETTINGS_KEY_NAME, cls.SETTINGS_SECRET_NAME))
+        return setting(cls.SETTINGS_KEY_NAME) and \
+               setting(cls.SETTINGS_SECRET_NAME)
 
 
 class BaseOAuth2(BaseOAuth):
@@ -588,7 +652,11 @@ class BaseOAuth2(BaseOAuth):
             raise ValueError('OAuth2 authentication failed: %s' % error)
         else:
             response.update(self.user_data(response['access_token']) or {})
-            kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
+            kwargs.update({
+                'auth': self,
+                'response': response,
+                self.AUTH_BACKEND.name: True
+            })
             return authenticate(*args, **kwargs)
 
     def get_scope(self):
@@ -610,12 +678,14 @@ if setting('SOCIAL_AUTH_IMPORT_BACKENDS'):
     from warnings import warn
     warn("SOCIAL_AUTH_IMPORT_SOURCES is deprecated")
 
+
 # Cache for discovered backends.
-BACKENDS = {}
+BACKENDSCACHE = {}
+
 
 def get_backends(force_load=False):
     """
-    Entry point to the BACKENDS cache. If BACKENDS hasn't been
+    Entry point to the BACKENDS cache. If BACKENDSCACHE hasn't been
     populated, each of the modules referenced in
     AUTHENTICATION_BACKENDS is imported and checked for a BACKENDS
     definition and if enabled, added to the cache.
@@ -632,18 +702,18 @@ def get_backends(force_load=False):
     A force_load boolean arg is also provided so that get_backend
     below can retry a requested backend that may not yet be discovered.
     """
-    if not BACKENDS or force_load:
-        for auth_backend in settings.AUTHENTICATION_BACKENDS:
+    if not BACKENDSCACHE or force_load:
+        for auth_backend in setting('AUTHENTICATION_BACKENDS'):
             module = import_module(auth_backend.rsplit(".", 1)[0])
             backends = getattr(module, "BACKENDS", {})
             for name, backend in backends.items():
                 if backend.enabled():
-                    BACKENDS[name] = backend
-    return BACKENDS
+                    BACKENDSCACHE[name] = backend
+    return BACKENDSCACHE
 
 
 def get_backend(name, *args, **kwargs):
-    """Returns a backend by name. Backends are stored in the BACKENDS
+    """Returns a backend by name. Backends are stored in the BACKENDSCACHE
     cache dict. If not found, each of the modules referenced in
     AUTHENTICATION_BACKENDS is imported and checked for a BACKENDS
     definition. If the named backend is found in the module's BACKENDS
@@ -651,12 +721,17 @@ def get_backend(name, *args, **kwargs):
     """
     try:
         # Cached backend which has previously been discovered.
-        return BACKENDS[name](*args, **kwargs)
+        return BACKENDSCACHE[name](*args, **kwargs)
     except KeyError:
         # Force a reload of BACKENDS to ensure a missing
         # backend hasn't been missed.
         get_backends(force_load=True)
         try:
-            return BACKENDS[name](*args, **kwargs)
+            return BACKENDSCACHE[name](*args, **kwargs)
         except KeyError:
             return None
+
+
+BACKENDS = {
+    'openid': OpenIdAuth
+}
diff --git a/social_auth/backends/browserid.py b/social_auth/backends/browserid.py
new file mode 100644 (file)
index 0000000..a8774b5
--- /dev/null
@@ -0,0 +1,90 @@
+"""
+BrowserID support
+"""
+import time
+from datetime import datetime
+from urllib import urlencode
+from urllib2 import urlopen
+
+from django.contrib.auth import authenticate
+from django.utils import simplejson
+
+from social_auth.backends import SocialAuthBackend, BaseAuth, USERNAME
+from social_auth.utils import log, setting
+
+
+# BrowserID verification server
+BROWSER_ID_SERVER = 'https://browserid.org/verify'
+
+
+class BrowserIDBackend(SocialAuthBackend):
+    """BrowserID authentication backend"""
+    name = 'browserid'
+
+    def get_user_id(self, details, response):
+        """Use BrowserID email as ID"""
+        return details['email']
+
+    def get_user_details(self, response):
+        """Return user details, BrowserID only provides Email."""
+        # {'status': 'okay',
+        #  'audience': 'localhost:8000',
+        #  'expires': 1328983575529,
+        #  'email': 'name@server.com',
+        #  'issuer': 'browserid.org'}
+        email = response['email']
+        return {USERNAME: email.split('@', 1)[0],
+                'email': email,
+                'fullname': '',
+                'first_name': '',
+                'last_name': ''}
+
+    def extra_data(self, user, uid, response, details):
+        """Return users extra data"""
+        # BrowserID sends timestamp for expiration date, here we
+        # comvert it to the remaining seconds
+        expires = (response['expires'] / 1000) - \
+                  time.mktime(datetime.now().timetuple())
+        return {
+            'audience': response['audience'],
+            'issuer': response['issuer'],
+            setting('SOCIAL_AUTH_EXPIRATION', 'expires'): expires
+        }
+
+
+# Auth classes
+class BrowserIDAuth(BaseAuth):
+    """BrowserID authentication"""
+    AUTH_BACKEND = BrowserIDBackend
+
+    def auth_complete(self, *args, **kwargs):
+        """Completes loging process, must return user instance"""
+        if not 'assertion' in self.data:
+            raise ValueError('Missing assertion parameter')
+
+        data = urlencode({
+            'assertion': self.data['assertion'],
+            'audience': self.request.get_host()
+        })
+
+        try:
+            response = simplejson.load(urlopen(BROWSER_ID_SERVER, data=data))
+        except ValueError:
+            log('error', 'Could not load user data from BrowserID.',
+                exc_info=True)
+        else:
+            if response.get('status') == 'failure':
+                log('debug', 'Authentication failed.')
+                raise ValueError('Authentication failed')
+
+            kwargs.update({
+                'response': response,
+                self.AUTH_BACKEND.name: True
+            })
+            return authenticate(*args, **kwargs)
+
+
+# Backend definition
+BACKENDS = {
+    'browserid': BrowserIDAuth
+}
index 0a140fbecddef7c91627e3f890f0887a8fc29eee..33a484bc5df431eda9beb6f107947feafba6ffd7 100644 (file)
@@ -8,9 +8,9 @@ given by Dropbox application registration process.
 By default account id and token expiration time are stored in extra_data
 field, check OAuthBackend class for details on how to extend it.
 """
-from django.conf import settings
 from django.utils import simplejson
 
+from social_auth.utils import setting
 from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME
 
 
@@ -20,14 +20,16 @@ DROPBOX_API = 'api.%s' % DROPBOX_SERVER
 DROPBOX_REQUEST_TOKEN_URL = 'https://%s/1/oauth/request_token' % DROPBOX_API
 DROPBOX_AUTHORIZATION_URL = 'https://www.%s/1/oauth/authorize' % DROPBOX_SERVER
 DROPBOX_ACCESS_TOKEN_URL = 'https://%s/1/oauth/access_token' % DROPBOX_API
-EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
 
 
 class DropboxBackend(OAuthBackend):
     """Dropbox OAuth authentication backend"""
     name = 'dropbox'
     # Default extra data to store
-    EXTRA_DATA = [('id', 'id'), ('expires', EXPIRES_NAME)]
+    EXTRA_DATA = [
+        ('id', 'id'),
+        ('expires', setting('SOCIAL_AUTH_EXPIRATION', 'expires'))
+    ]
 
     def get_user_details(self, response):
         """Return user details from Dropbox account"""
@@ -64,9 +66,7 @@ class DropboxAuth(ConsumerBasedOAuth):
     @classmethod
     def enabled(cls):
         """Return backend enabled status by checking basic settings"""
-        return all(hasattr(settings, name) for name in
-                        ('DROPBOX_APP_ID',
-                         'DROPBOX_API_SECRET'))
+        return setting('DROPBOX_APP_ID') and setting('DROPBOX_API_SECRET')
 
 
 # Backend definition
index b61db38d339a335564b9d08293bbd731693c578b..b6ba55b351e64c54ec9f7e20857815a71bd62afc 100644 (file)
@@ -10,14 +10,14 @@ extra_data field, check OAuthBackend class for details on how to extend it.
 """
 try:
     from urlparse import parse_qs
-    parse_qs # placate pyflakes
+    parse_qs  # placate pyflakes
 except ImportError:
     # fall back for Python 2.5
     from cgi import parse_qs
 
-from django.conf import settings
-
 from oauth2 import Token
+
+from social_auth.utils import setting
 from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME
 
 
@@ -26,16 +26,17 @@ FLICKR_SERVER = 'http://www.flickr.com/services'
 FLICKR_REQUEST_TOKEN_URL = '%s/oauth/request_token' % FLICKR_SERVER
 FLICKR_AUTHORIZATION_URL = '%s/oauth/authorize' % FLICKR_SERVER
 FLICKR_ACCESS_TOKEN_URL = '%s/oauth/access_token' % FLICKR_SERVER
-EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
 
 
 class FlickrBackend(OAuthBackend):
     """Flickr OAuth authentication backend"""
     name = 'flickr'
     # Default extra data to store
-    EXTRA_DATA = [('id', 'id'),
-                  ('username', 'username'),
-                  ('expires', EXPIRES_NAME)]
+    EXTRA_DATA = [
+        ('id', 'id'),
+        ('username', 'username'),
+        ('expires', setting('SOCIAL_AUTH_EXPIRATION', 'expires'))
+    ]
 
     def get_user_details(self, response):
         """Return user details from Flickr account"""
index 9e70d9ef89590e4b02e52c2d5e3cec14d6a36161..0ba718a1c27169ed7cf71294fc886ebb8d18f8e2 100644 (file)
@@ -1,6 +1,4 @@
 import urllib
-import logging
-logger = logging.getLogger(__name__)
 
 from django.utils import simplejson
 
index 3b138356a59735fd2aa236ee11857f9931951e54..b866ca35979f785c28e375e09935aad8f1b25853 100644 (file)
@@ -13,14 +13,11 @@ field, check OAuthBackend class for details on how to extend it.
 """
 import cgi
 import urllib
-import logging
-logger = logging.getLogger(__name__)
 
-
-from django.conf import settings
 from django.utils import simplejson
 from django.contrib.auth import authenticate
 
+from social_auth.utils import setting
 from social_auth.backends import BaseOAuth, OAuthBackend, USERNAME
 
 
@@ -29,14 +26,16 @@ GITHUB_SERVER = 'github.com'
 GITHUB_AUTHORIZATION_URL = 'https://%s/login/oauth/authorize' % GITHUB_SERVER
 GITHUB_ACCESS_TOKEN_URL = 'https://%s/login/oauth/access_token' % GITHUB_SERVER
 GITHUB_API_URL = 'https://api.%s' % GITHUB_SERVER
-EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
 
 
 class GithubBackend(OAuthBackend):
     """Github OAuth authentication backend"""
     name = 'github'
     # Default extra data to store
-    EXTRA_DATA = [('id', 'id'), ('expires', EXPIRES_NAME)]
+    EXTRA_DATA = [
+        ('id', 'id'),
+        ('expires', setting('SOCIAL_AUTH_EXPIRATION', 'expires'))
+    ]
 
     def get_user_details(self, response):
         """Return user details from Github account"""
@@ -50,21 +49,22 @@ class GithubAuth(BaseOAuth):
 
     def auth_url(self):
         """Returns redirect url"""
-        args = {'client_id': settings.GITHUB_APP_ID,
+        args = {'client_id': setting('GITHUB_APP_ID'),
                 'redirect_uri': self.redirect_uri}
-        if hasattr(settings, 'GITHUB_EXTENDED_PERMISSIONS'):
-            args['scope'] = ','.join(settings.GITHUB_EXTENDED_PERMISSIONS)
+        if setting('GITHUB_EXTENDED_PERMISSIONS'):
+            args['scope'] = ','.join(setting('GITHUB_EXTENDED_PERMISSIONS'))
         args.update(self.auth_extra_arguments())
         return GITHUB_AUTHORIZATION_URL + '?' + urllib.urlencode(args)
 
     def auth_complete(self, *args, **kwargs):
         """Returns user, might be logged in"""
         if 'code' in self.data:
-            url = GITHUB_ACCESS_TOKEN_URL + '?' + \
-                  urllib.urlencode({'client_id': settings.GITHUB_APP_ID,
-                                'redirect_uri': self.redirect_uri,
-                                'client_secret': settings.GITHUB_API_SECRET,
-                                'code': self.data['code']})
+            url = GITHUB_ACCESS_TOKEN_URL + '?' + urllib.urlencode({
+                  'client_id': setting('GITHUB_APP_ID'),
+                  'redirect_uri': self.redirect_uri,
+                  'client_secret': setting('GITHUB_API_SECRET'),
+                  'code': self.data['code']
+            })
             response = cgi.parse_qs(urllib.urlopen(url).read())
             if response.get('error'):
                 error = self.data.get('error') or 'unknown error'
@@ -94,9 +94,7 @@ class GithubAuth(BaseOAuth):
     @classmethod
     def enabled(cls):
         """Return backend enabled status by checking basic settings"""
-        return all(hasattr(settings, name) for name in
-                        ('GITHUB_APP_ID',
-                         'GITHUB_API_SECRET'))
+        return setting('GITHUB_APP_ID') and setting('GITHUB_API_SECRET')
 
 
 # Backend definition
diff --git a/social_auth/backends/contrib/instagram.py b/social_auth/backends/contrib/instagram.py
new file mode 100644 (file)
index 0000000..e2fba7d
--- /dev/null
@@ -0,0 +1,54 @@
+import urllib
+
+from django.utils import simplejson
+
+from social_auth.backends import BaseOAuth2, OAuthBackend, USERNAME
+
+
+INSTAGRAM_SERVER = 'instagram.com'
+INSTAGRAM_AUTHORIZATION_URL = 'https://instagram.com/oauth/authorize'
+INSTAGRAM_ACCESS_TOKEN_URL = 'https://instagram.com/oauth/access_token'
+INSTAGRAM_CHECK_AUTH = 'https://api.instagram.com/v1/users/self'
+
+
+class InstagramBackend(OAuthBackend):
+    name = 'instagram'
+
+    def get_user_id(self, details, response):
+        return response['user']['id']
+
+    def get_user_details(self, response):
+        """Return user details from Instagram account"""
+        username = response['user']['username']
+        fullname = response['user'].get('fullname', '')
+        email = response['user'].get('email', '')
+        return {
+            USERNAME: username,
+            'first_name': fullname,
+            'email': email
+        }
+
+
+class InstagramAuth(BaseOAuth2):
+    """Instagram OAuth mechanism"""
+    AUTHORIZATION_URL = INSTAGRAM_AUTHORIZATION_URL
+    ACCESS_TOKEN_URL = INSTAGRAM_ACCESS_TOKEN_URL
+    SERVER_URL = INSTAGRAM_SERVER
+    AUTH_BACKEND = InstagramBackend
+    SETTINGS_KEY_NAME = 'INSTAGRAM_CLIENT_ID'
+    SETTINGS_SECRET_NAME = 'INSTAGRAM_CLIENT_SECRET'
+
+    def user_data(self, access_token):
+        """Loads user data from service"""
+        params = {'access_token': access_token,}
+        url = INSTAGRAM_CHECK_AUTH + '?' + urllib.urlencode(params)
+        try:
+            return simplejson.load(urllib.urlopen(url))
+        except ValueError:
+            return None
+
+
+# Backend definition
+BACKENDS = {
+    'instagram': InstagramAuth,
+}
index 00ac92bf0ff3fd1a8a2288af9184a8d42c5909cc..ae9271655b1a2d4427cc9b4c2623f2e595d408d8 100644 (file)
@@ -3,14 +3,10 @@ Linkedin OAuth support
 
 No extra configurations are needed to make this work.
 """
-import logging
-logger = logging.getLogger(__name__)
-
 from xml.etree import ElementTree
 from xml.parsers.expat import ExpatError
 
-from django.conf import settings
-
+from social_auth.utils import setting
 from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME
 
 
@@ -57,8 +53,7 @@ class LinkedinAuth(ConsumerBasedOAuth):
     def user_data(self, access_token):
         """Return user data provided"""
         fields_selectors = LINKEDIN_FIELD_SELECTORS + \
-                           getattr(settings, 'LINKEDIN_EXTRA_FIELD_SELECTORS',
-                                   [])
+                           setting('LINKEDIN_EXTRA_FIELD_SELECTORS', [])
         url = LINKEDIN_CHECK_AUTH + ':(%s)' % ','.join(fields_selectors)
         request = self.oauth_request(access_token, url)
         raw_xml = self.fetch_response(request)
index 2e516a05048803b68014e65d92de4ac1bf284d44..ce9e538d9d5bb40f81511a9fbc862b2048748c08 100644 (file)
@@ -5,8 +5,6 @@ This contribution adds support for LiveJournal OpenID service in the form
 username.livejournal.com. Username is retrieved from the identity url.
 """
 import urlparse
-import logging
-logger = logging.getLogger(__name__)
 
 from social_auth.backends import OpenIDBackend, OpenIdAuth, USERNAME
 
index c426dabb10aa88dca5213d7ccfbc0311b1fd88cc..c7ed4d2a424e7006a0222f7137138b99c8c81bab 100644 (file)
@@ -11,12 +11,10 @@ OAuth settings ORKUT_CONSUMER_KEY and ORKUT_CONSUMER_SECRET are needed
 to enable this service support.
 """
 import urllib
-import logging
-logger = logging.getLogger(__name__)
 
-from django.conf import settings
 from django.utils import simplejson
 
+from social_auth.utils import setting
 from social_auth.backends import OAuthBackend, USERNAME
 from social_auth.backends.google import BaseGoogleOAuth
 
@@ -56,9 +54,9 @@ class OrkutAuth(BaseGoogleOAuth):
     def user_data(self, access_token):
         """Loads user data from Orkut service"""
         fields = ORKUT_DEFAULT_DATA
-        if hasattr(settings, 'ORKUT_EXTRA_DATA'):
-            fields += ',' + settings.ORKUT_EXTRA_DATA
-        scope = ORKUT_SCOPE + getattr(settings, 'ORKUT_EXTRA_SCOPE', [])
+        if setting('ORKUT_EXTRA_DATA'):
+            fields += ',' + setting('ORKUT_EXTRA_DATA')
+        scope = ORKUT_SCOPE + setting('ORKUT_EXTRA_SCOPE', [])
         params = {'method': 'people.get',
                   'id': 'myself',
                   'userId': '@me',
@@ -74,7 +72,7 @@ class OrkutAuth(BaseGoogleOAuth):
 
     def oauth_request(self, token, url, extra_params=None):
         extra_params = extra_params or {}
-        scope = ORKUT_SCOPE + getattr(settings, 'ORKUT_EXTRA_SCOPE', [])
+        scope = ORKUT_SCOPE + setting('ORKUT_EXTRA_SCOPE', [])
         extra_params['scope'] = ' '.join(scope)
         return super(OrkutAuth, self).oauth_request(token, url, extra_params)
 
index 8e5053eff805adb22cd4c6864300ce9b8c754c9f..aca23815505d08225acc422a17f7a2449bef3368 100644 (file)
@@ -11,10 +11,6 @@ setting, it must be a list of values to request.
 By default account id and token expiration time are stored in extra_data
 field, check OAuthBackend class for details on how to extend it.
 """
-import logging
-logger = logging.getLogger(__name__)
-
-
 import cgi
 from urllib import urlencode
 from urllib2 import urlopen
@@ -23,16 +19,14 @@ import hmac
 import hashlib
 import time
 
-from django.conf import settings
 from django.utils import simplejson
 from django.contrib.auth import authenticate
 
 from social_auth.backends import BaseOAuth2, OAuthBackend, USERNAME
-from social_auth.utils import sanitize_log_data
+from social_auth.utils import sanitize_log_data, setting, log
 
 
 # Facebook configuration
-EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
 FACEBOOK_ME = 'https://graph.facebook.com/me?'
 
 
@@ -40,13 +34,16 @@ class FacebookBackend(OAuthBackend):
     """Facebook OAuth2 authentication backend"""
     name = 'facebook'
     # Default extra data to store
-    EXTRA_DATA = [('id', 'id'), ('expires', EXPIRES_NAME)]
+    EXTRA_DATA = [
+        ('id', 'id'),
+        ('expires', setting('SOCIAL_AUTH_EXPIRATION', 'expires'))
+    ]
 
     def get_user_details(self, response):
         """Return user details from Facebook account"""
         return {USERNAME: response.get('username'),
                 'email': response.get('email', ''),
-                'fullname': response['name'],
+                'fullname': response.get('name', ''),
                 'first_name': response.get('first_name', ''),
                 'last_name': response.get('last_name', '')}
 
@@ -61,7 +58,7 @@ class FacebookAuth(BaseOAuth2):
     SETTINGS_SECRET_NAME = 'FACEBOOK_API_SECRET'
 
     def get_scope(self):
-        return getattr(settings, 'FACEBOOK_EXTENDED_PERMISSIONS', [])
+        return setting('FACEBOOK_EXTENDED_PERMISSIONS', [])
 
     def user_data(self, access_token):
         """Loads user data from service"""
@@ -70,13 +67,14 @@ class FacebookAuth(BaseOAuth2):
 
         try:
             data = simplejson.load(urlopen(url))
-            logger.debug('Found user data for token %s',
-                         sanitize_log_data(access_token),
-                         extra=dict(data=data))
         except ValueError:
             extra = {'access_token': sanitize_log_data(access_token)}
-            logger.error('Could not load user data from Facebook.',
-                         exc_info=True, extra=extra)
+            log('error', 'Could not load user data from Facebook.',
+                exc_info=True, extra=extra)
+        else:
+            log('debug', 'Found user data for token %s',
+                sanitize_log_data(access_token),
+                extra=dict(data=data))
         return data
 
     def auth_complete(self, *args, **kwargs):
@@ -86,9 +84,9 @@ class FacebookAuth(BaseOAuth2):
 
         if 'code' in self.data:
             url = 'https://graph.facebook.com/oauth/access_token?' + \
-                  urlencode({'client_id': settings.FACEBOOK_APP_ID,
+                  urlencode({'client_id': setting('FACEBOOK_APP_ID'),
                              'redirect_uri': self.redirect_uri,
-                             'client_secret': settings.FACEBOOK_API_SECRET,
+                             'client_secret': setting('FACEBOOK_API_SECRET'),
                              'code': self.data['code']})
             response = cgi.parse_qs(urlopen(url).read())
             access_token = response['access_token'][0]
@@ -125,8 +123,7 @@ class FacebookAuth(BaseOAuth2):
     @classmethod
     def enabled(cls):
         """Return backend enabled status by checking basic settings"""
-        return all(hasattr(settings, name) for name in ('FACEBOOK_APP_ID',
-                                                        'FACEBOOK_API_SECRET'))
+        return setting('FACEBOOK_APP_ID') and setting('FACEBOOK_API_SECRET')
 
 
 def base64_url_decode(data):
index 21b545335250e282f102a0aee9fabd616b0ae501..88896abc89f4a099bfda5e3466fe19c38ec42a73 100644 (file)
@@ -13,26 +13,23 @@ APIs console https://code.google.com/apis/console/ Identity option.
 
 OpenID also works straightforward, it doesn't need further configurations.
 """
-import logging
-logger = logging.getLogger(__name__)
-
 from urllib import urlencode
 from urllib2 import Request, urlopen
 
 from oauth2 import Request as OAuthRequest
 
-from django.conf import settings
 from django.utils import simplejson
 
+from social_auth.utils import setting
 from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, BaseOAuth2, \
                                  OAuthBackend, OpenIDBackend, USERNAME
 
 
 # Google OAuth base configuration
 GOOGLE_OAUTH_SERVER = 'www.google.com'
-GOOGLE_OAUTH_AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
-GOOGLE_OAUTH_REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
-GOOGLE_OAUTH_ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
+AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
+REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
+ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
 
 # Google OAuth2 base configuration
 GOOGLE_OAUTH2_SERVER = 'accounts.google.com'
@@ -44,8 +41,6 @@ GOOGLE_OAUTH_SCOPE = ['https://www.googleapis.com/auth/userinfo#email']
 GOOGLEAPIS_EMAIL = 'https://www.googleapis.com/userinfo/email'
 GOOGLE_OPENID_URL = 'https://www.google.com/accounts/o8/id'
 
-EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
-
 
 # Backends
 class GoogleOAuthBackend(OAuthBackend):
@@ -69,8 +64,10 @@ class GoogleOAuthBackend(OAuthBackend):
 class GoogleOAuth2Backend(GoogleOAuthBackend):
     """Google OAuth2 authentication backend"""
     name = 'google-oauth2'
-    EXTRA_DATA = [('refresh_token', 'refresh_token'),
-                  ('expires_in', EXPIRES_NAME)]
+    EXTRA_DATA = [
+        ('refresh_token', 'refresh_token'),
+        ('expires_in', setting('SOCIAL_AUTH_EXPIRATION', 'expires'))
+    ]
 
 
 class GoogleBackend(OpenIDBackend):
@@ -78,11 +75,19 @@ class GoogleBackend(OpenIDBackend):
     name = 'google'
 
     def get_user_id(self, details, response):
-        """Return user unique id provided by service. For google user email
+        """
+        Return user unique id provided by service. For google user email
         is unique enought to flag a single user. Email comes from schema:
-        http://axschema.org/contact/email"""
+        http://axschema.org/contact/email
+        """
+        # White listed domains (accepts all if list is empty)
+        domains = setting('GOOGLE_WHITE_LISTED_DOMAINS', [])
+        if domains and details['email'].split('@', 1)[1] not in domains:
+            raise ValueError('Domain not allowed')
+
         return details['email']
 
+
 # Auth classes
 class GoogleAuth(OpenIdAuth):
     """Google OpenID authentication"""
@@ -95,9 +100,9 @@ class GoogleAuth(OpenIdAuth):
 
 class BaseGoogleOAuth(ConsumerBasedOAuth):
     """Base class for Google OAuth mechanism"""
-    AUTHORIZATION_URL = GOOGLE_OAUTH_AUTHORIZATION_URL
-    REQUEST_TOKEN_URL = GOOGLE_OAUTH_REQUEST_TOKEN_URL
-    ACCESS_TOKEN_URL = GOOGLE_OAUTH_ACCESS_TOKEN_URL
+    AUTHORIZATION_URL = AUTHORIZATION_URL
+    REQUEST_TOKEN_URL = REQUEST_TOKEN_URL
+    ACCESS_TOKEN_URL = ACCESS_TOKEN_URL
     SERVER_URL = GOOGLE_OAUTH_SERVER
 
     def user_data(self, access_token):
@@ -126,14 +131,12 @@ class GoogleOAuth(BaseGoogleOAuth):
 
     def oauth_request(self, token, url, extra_params=None):
         extra_params = extra_params or {}
-        scope = GOOGLE_OAUTH_SCOPE + \
-                getattr(settings, 'GOOGLE_OAUTH_EXTRA_SCOPE', [])
+        scope = GOOGLE_OAUTH_SCOPE + setting('GOOGLE_OAUTH_EXTRA_SCOPE', [])
         extra_params.update({
             'scope': ' '.join(scope),
         })
         if not self.registered():
-            xoauth_displayname = getattr(settings, 'GOOGLE_DISPLAY_NAME',
-                                         'Social Auth')
+            xoauth_displayname = setting('GOOGLE_DISPLAY_NAME', 'Social Auth')
             extra_params['xoauth_displayname'] = xoauth_displayname
         return super(GoogleOAuth, self).oauth_request(token, url, extra_params)
 
@@ -159,7 +162,7 @@ class GoogleOAuth(BaseGoogleOAuth):
 
 
 # TODO: Remove this setting name check, keep for backward compatibility
-_OAUTH2_KEY_NAME = hasattr(settings, 'GOOGLE_OAUTH2_CLIENT_ID') and \
+_OAUTH2_KEY_NAME = setting('GOOGLE_OAUTH2_CLIENT_ID') and \
                    'GOOGLE_OAUTH2_CLIENT_ID' or \
                    'GOOGLE_OAUTH2_CLIENT_KEY'
 
@@ -173,8 +176,7 @@ class GoogleOAuth2(BaseOAuth2):
     SETTINGS_SECRET_NAME = 'GOOGLE_OAUTH2_CLIENT_SECRET'
 
     def get_scope(self):
-        return GOOGLE_OAUTH_SCOPE + \
-               getattr(settings, 'GOOGLE_OAUTH_EXTRA_SCOPE', [])
+        return GOOGLE_OAUTH_SCOPE + setting('GOOGLE_OAUTH_EXTRA_SCOPE', [])
 
     def user_data(self, access_token):
         """Return user data from Google API"""
index 6af8113e062b3670e5d662bc3a720f9569e71551..ae04cded401036096ead9ceafdb40305c61a3862 100644 (file)
@@ -10,6 +10,7 @@ import warnings
 from django.conf import settings
 
 from social_auth.models import User
+from social_auth.backends import get_backend, PIPELINE
 
 
 USERNAME = 'username'
index 1e260fd199f78add2fd74abf18a462f343e6ccfc..18c9c46993d6c3aafb35b4a769e6ecc5d1827bdf 100644 (file)
@@ -1,6 +1,6 @@
-from django.conf import settings
 from django.core.exceptions import MultipleObjectsReturned
 
+from social_auth.utils import setting
 from social_auth.models import User
 from social_auth.backends.pipeline import warn_setting
 
@@ -11,7 +11,7 @@ def associate_by_email(details, *args, **kwargs):
 
     warn_setting('SOCIAL_AUTH_ASSOCIATE_BY_MAIL', 'associate_by_email')
 
-    if email and getattr(settings, 'SOCIAL_AUTH_ASSOCIATE_BY_MAIL', False):
+    if email and setting('SOCIAL_AUTH_ASSOCIATE_BY_MAIL'):
         # try to associate accounts registered with the same email address,
         # only if it's a single object. ValueError is raised if multiple
         # objects are returned
diff --git a/social_auth/backends/pipeline/misc.py b/social_auth/backends/pipeline/misc.py
new file mode 100644 (file)
index 0000000..22b0e19
--- /dev/null
@@ -0,0 +1,24 @@
+from social_auth.backends import PIPELINE
+from social_auth.utils import setting
+
+
+PIPELINE_ENTRY = 'social_auth.backends.pipeline.misc.save_status_to_session'
+
+
+def save_status_to_session(request, auth, *args, **kwargs):
+    """Saves current social-auth status to session."""
+    next_entry = setting('SOCIAL_AUTH_PIPELINE_RESUME_ENTRY')
+
+    try:
+        if next_entry:
+            idx = PIPELINE.index(next_entry)
+        else:
+            idx = PIPELINE.index(PIPELINE_ENTRY) + 1
+    except ValueError:
+        idx = None
+
+    data = auth.to_session_dict(idx, *args, **kwargs)
+
+    name = setting('SOCIAL_AUTH_PARTIAL_PIPELINE_KEY', 'partial_pipeline')
+    request.session[name] = data
+    request.session.modified = True
index 604fb8c2200e1a99cba45e53d3b72407dc7bb174..d8436166d20c6cd8165cec91a32bbbbb47cac7bb 100644 (file)
@@ -1,6 +1,6 @@
-from django.conf import settings
 from django.db.utils import IntegrityError
 
+from social_auth.utils import setting
 from social_auth.models import UserSocialAuth
 from social_auth.backends.pipeline import warn_setting
 
@@ -51,7 +51,7 @@ def load_extra_data(backend, details, response, social_user, uid, user,
     """
     warn_setting('SOCIAL_AUTH_EXTRA_DATA', 'load_extra_data')
 
-    if getattr(settings, 'SOCIAL_AUTH_EXTRA_DATA', True):
+    if setting('SOCIAL_AUTH_EXTRA_DATA', True):
         extra_data = backend.extra_data(user, uid, response, details)
         if extra_data and social_user.extra_data != extra_data:
             social_user.extra_data = extra_data
index eaca1ae261658655235e3c7f440e0ab04ba779b4..3c9ef220f2dbbf1bfb22b0cb3c85e724a80b733c 100644 (file)
@@ -1,7 +1,6 @@
 from uuid import uuid4
 
-from django.conf import settings
-
+from social_auth.utils import setting
 from social_auth.models import User
 from social_auth.backends.pipeline import USERNAME, USERNAME_MAX_LENGTH, \
                                           warn_setting
@@ -10,7 +9,14 @@ from social_auth.signals import socialauth_not_registered, \
                                 pre_update
 
 
-def get_username(details, user=None, *args, **kwargs):
+def simple_user_exists(*args, **kwargs):
+    """Return True/False if a User instance exists with the given arguments.
+    Arguments are directly passed to filter() manager method."""
+    return User.objects.filter(*args, **kwargs).exists()
+
+
+def get_username(details, user=None, user_exists=simple_user_exists,
+                 *args, **kwargs):
     """Return an username for new user. Return current user username
     if user was given.
     """
@@ -22,37 +28,30 @@ def get_username(details, user=None, *args, **kwargs):
     warn_setting('SOCIAL_AUTH_UUID_LENGTH', 'get_username')
     warn_setting('SOCIAL_AUTH_USERNAME_FIXER', 'get_username')
 
-    if getattr(settings, 'SOCIAL_AUTH_FORCE_RANDOM_USERNAME', False):
+    if setting('SOCIAL_AUTH_FORCE_RANDOM_USERNAME'):
         username = uuid4().get_hex()
     elif details.get(USERNAME):
         username = details[USERNAME]
-    elif hasattr(settings, 'SOCIAL_AUTH_DEFAULT_USERNAME'):
-        username = settings.SOCIAL_AUTH_DEFAULT_USERNAME
+    elif setting('SOCIAL_AUTH_DEFAULT_USERNAME'):
+        username = setting('SOCIAL_AUTH_DEFAULT_USERNAME')
         if callable(username):
             username = username()
     else:
         username = uuid4().get_hex()
 
-    uuid_lenght = getattr(settings, 'SOCIAL_AUTH_UUID_LENGTH', 16)
-    username_fixer = getattr(settings, 'SOCIAL_AUTH_USERNAME_FIXER',
-                             lambda u: u)
+    uuid_lenght = setting('SOCIAL_AUTH_UUID_LENGTH', 16)
+    username_fixer = setting('SOCIAL_AUTH_USERNAME_FIXER', lambda u: u)
 
     short_username = username[:USERNAME_MAX_LENGTH - uuid_lenght]
-    final_username = None
+    final_username = username_fixer(username)[:USERNAME_MAX_LENGTH]
 
-    while True:
+    # Generate a unique username for current user using username
+    # as base but adding a unique hash at the end. Original
+    # username is cut to avoid any field max_length.
+    while user_exists(username=final_username):
+        username = short_username + uuid4().get_hex()[:uuid_lenght]
         final_username = username_fixer(username)[:USERNAME_MAX_LENGTH]
 
-        try:
-            User.objects.get(username=final_username)
-        except User.DoesNotExist:
-            break
-        else:
-            # User with same username already exists, generate a unique
-            # username for current user using username as base but adding
-            # a unique hash at the end. Original username is cut to avoid
-            # the field max_length.
-            username = short_username + uuid4().get_hex()[:uuid_lenght]
     return {'username': final_username}
 
 
@@ -66,7 +65,7 @@ def create_user(backend, details, response, uid, username, user=None, *args,
 
     warn_setting('SOCIAL_AUTH_CREATE_USERS', 'create_user')
 
-    if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', True):
+    if not setting('SOCIAL_AUTH_CREATE_USERS', True):
         # Send signal for cases where tracking failed registering is useful.
         socialauth_not_registered.send(sender=backend.__class__,
                                        uid=uid,
@@ -89,7 +88,7 @@ def update_user_details(backend, details, response, user, is_new=False, *args,
     warn_setting('SOCIAL_AUTH_CHANGE_SIGNAL_ONLY', 'update_user_details')
 
     # check if values update should be left to signals handlers only
-    if not getattr(settings, 'SOCIAL_AUTH_CHANGE_SIGNAL_ONLY', False):
+    if not setting('SOCIAL_AUTH_CHANGE_SIGNAL_ONLY'):
         for name, value in details.iteritems():
             # do not update username, it was already generated
             if name in (USERNAME, 'id', 'pk'):
index 759fe792b33a2254b6eac2e62b3afbcf85b90058..13fc7d13769622ade496f5c40eb7eb92f9862e9d 100644 (file)
@@ -11,9 +11,6 @@ User screen name is used to generate username.
 By default account id is stored in extra_data field, check OAuthBackend
 class for details on how to extend it.
 """
-import logging
-logger = logging.getLogger(__name__)
-
 from django.utils import simplejson
 
 from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME
@@ -67,6 +64,13 @@ class TwitterAuth(ConsumerBasedOAuth):
         except ValueError:
             return None
 
+    def auth_complete(self, *args, **kwargs):
+        """Completes loging process, must return user instance"""
+        if 'denied' in self.data:
+            raise ValueError('Authentication denied')
+        else:
+            return super(TwitterAuth, self).auth_complete(*args, **kwargs)
+
 
 # Backend definition
 BACKENDS = {
index e17577e432d8f3c3430c39191bc00dcd64bea64f..65ff401a410aca3043ab23ebffbd37384dce16e3 100644 (file)
@@ -3,13 +3,10 @@ Yahoo OpenID support
 
 No extra configurations are needed to make this work.
 """
-import logging
-logger = logging.getLogger(__name__)
-
 from social_auth.backends import OpenIDBackend, OpenIdAuth
 
 
-YAHOO_OPENID_URL = 'http://yahoo.com'
+YAHOO_OPENID_URL = 'http://me.yahoo.com'
 
 
 class YahooBackend(OpenIDBackend):
index 47bf4c65e7dc70e73b4f4ca96df0163ae5d27246..1355be1d202214f2a8cf7cfde3fd7bcb7f84791e 100644 (file)
@@ -36,10 +36,11 @@ def social_auth_by_name_backends(request):
     """
     keys = get_backends().keys()
     accounts = dict(zip(keys, [None] * len(keys)))
+    user = request.user
 
-    if isinstance(request.user, User) and request.user.is_authenticated():
-        for associated in request.user.social_auth.all():
-            accounts[associated.provider.replace('-', '_')] = associated
+    if isinstance(user, User) and user.is_authenticated():
+        accounts.update((assoc.provider.replace('-', '_'), assoc)
+                            for assoc in user.social_auth.all())
 
     return {'social_auth': accounts}
 
index 6d052017e4eeec0862488f0a6b3dfdf691f059fb..b700329647185c6f965af833b386dab663ed7ed4 100644 (file)
@@ -2,9 +2,9 @@
 from datetime import timedelta
 
 from django.db import models
-from django.conf import settings
 
 from social_auth.fields import JSONField
+from social_auth.utils import setting
 
 
 # If User class is overridden, it *must* provide the following fields
@@ -16,8 +16,8 @@ from social_auth.fields import JSONField
 #   def is_authenticated():
 #       ...
 
-if getattr(settings, 'SOCIAL_AUTH_USER_MODEL', None):
-    User = models.get_model(*settings.SOCIAL_AUTH_USER_MODEL.rsplit('.', 1))
+if setting('SOCIAL_AUTH_USER_MODEL'):
+    User = models.get_model(*setting('SOCIAL_AUTH_USER_MODEL').rsplit('.', 1))
 else:
     from django.contrib.auth.models import User
 
@@ -43,7 +43,7 @@ class UserSocialAuth(models.Model):
         value stored or it's malformed.
         """
         if self.extra_data:
-            name = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
+            name = setting('SOCIAL_AUTH_EXPIRATION', 'expires')
             try:
                 return timedelta(seconds=int(self.extra_data.get(name)))
             except (ValueError, TypeError):
index bc143ccac2af403b8184680c5a552ca79ffd3945..74ed06f6b31e48f83f2465adb468b83fb7e36520 100644 (file)
@@ -1,8 +1,11 @@
-from django.conf import settings
+from social_auth.utils import setting
 
-if getattr(settings,'SOCIAL_AUTH_TEST_TWITTER', True):
+
+if setting('SOCIAL_AUTH_TEST_TWITTER', True):
     from social_auth.tests.twitter import *
-if getattr(settings,'SOCIAL_AUTH_TEST_FACEBOOK', True):
+
+if setting('SOCIAL_AUTH_TEST_FACEBOOK', True):
     from social_auth.tests.facebook import *
-if getattr(settings,'SOCIAL_AUTH_TEST_GOOGLE', True):
+
+if setting('SOCIAL_AUTH_TEST_GOOGLE', True):
     from social_auth.tests.google import *
index 1eb27359534bbea98d98d6a7ae91d29021bf5aeb..74d5b47e752b10ae9acaacbc6c1bfa4227465f51 100644 (file)
@@ -1,7 +1,6 @@
 import re
 
-from django.conf import settings
-
+from social_auth.utils import setting
 from social_auth.tests.base import SocialAuthTestsCase, FormParserByID
 
 
@@ -12,8 +11,8 @@ class FacebookTestCase(SocialAuthTestsCase):
 
     def setUp(self, *args, **kwargs):
         super(FacebookTestCase, self).setUp(*args, **kwargs)
-        self.user = getattr(settings, 'TEST_FACEBOOK_USER', None)
-        self.passwd = getattr(settings, 'TEST_FACEBOOK_PASSWORD', None)
+        self.user = setting('TEST_FACEBOOK_USER')
+        self.passwd = setting('TEST_FACEBOOK_PASSWORD')
         # check that user and password are setup properly
         self.assertTrue(self.user)
         self.assertTrue(self.passwd)
index 9f3cd1daede3021cdcf34768645a40177666baae..3beb5a9d7c6754823d52d890ee4d489b0de87701 100644 (file)
@@ -1,7 +1,6 @@
 import re
 
-from django.conf import settings
-
+from social_auth.utils import setting
 from social_auth.tests.base import SocialAuthTestsCase, FormParserByID, \
                                    FormParser, RefreshParser
 
@@ -12,8 +11,8 @@ class GoogleTestCase(SocialAuthTestsCase):
 
     def setUp(self, *args, **kwargs):
         super(GoogleTestCase, self).setUp(*args, **kwargs)
-        self.user = getattr(settings, 'TEST_GOOGLE_USER', None)
-        self.passwd = getattr(settings, 'TEST_GOOGLE_PASSWORD', None)
+        self.user = setting('TEST_GOOGLE_USER')
+        self.passwd = setting('TEST_GOOGLE_PASSWORD')
         # check that user and password are setup properly
         self.assertTrue(self.user)
         self.assertTrue(self.passwd)
@@ -65,4 +64,4 @@ class GoogleOpenIdTestLogin(GoogleTestCase):
             result = self.get_redirect(parser.action, parser.values, use_cookies=True)
 
         response = self.client.get(self.make_relative(result.headers['Location']))
-        self.assertTrue(settings.LOGIN_REDIRECT_URL in self.make_relative(response['Location']))
+        self.assertTrue(setting('LOGIN_REDIRECT_URL') in self.make_relative(response['Location']))
index 6cfc5c587f1c57ae90005f6e332bc54737bf31a7..ee5cc2342e5c82f75d522249a41f2706b58c952e 100644 (file)
@@ -1,5 +1,4 @@
-from django.conf import settings
-
+from social_auth.utils import setting
 from social_auth.tests.base import SocialAuthTestsCase, FormParserByID, \
                                    RefreshParser
 
@@ -10,8 +9,8 @@ class TwitterTestCase(SocialAuthTestsCase):
 
     def setUp(self, *args, **kwargs):
         super(TwitterTestCase, self).setUp(*args, **kwargs)
-        self.user = getattr(settings, 'TEST_TWITTER_USER', None)
-        self.passwd = getattr(settings, 'TEST_TWITTER_PASSWORD', None)
+        self.user = setting('TEST_TWITTER_USER')
+        self.passwd = setting('TEST_TWITTER_PASSWORD')
         # check that user and password are setup properly
         self.assertTrue(self.user)
         self.assertTrue(self.passwd)
@@ -60,5 +59,5 @@ class TwitterTestLogin(TwitterTestCase):
         response = self.client.get(self.make_relative(parser.value))
         self.assertEqual(response.status_code, 302)
         location = self.make_relative(response['Location'])
-        login_redirect = getattr(settings, 'LOGIN_REDIRECT_URL', '')
+        login_redirect = setting('LOGIN_REDIRECT_URL')
         self.assertTrue(location == login_redirect)
index e1bb193f9c52ce66e82feb62d8d45d74d874b109..3d41d3c5a36243a4b2b69644595c69446fff0de0 100644 (file)
@@ -6,12 +6,21 @@ from social_auth.views import auth, complete, associate, associate_complete, \
 
 
 urlpatterns = patterns('',
-    url(r'^login/(?P<backend>[^/]+)/$', auth, name='socialauth_begin'),
-    url(r'^complete/(?P<backend>[^/]+)/$', complete, name='socialauth_complete'),
-    url(r'^associate/(?P<backend>[^/]+)/$', associate, name='socialauth_associate_begin'),
+    # authentication
+    url(r'^login/(?P<backend>[^/]+)/$', auth,
+        name='socialauth_begin'),
+    url(r'^complete/(?P<backend>[^/]+)/$', complete,
+        name='socialauth_complete'),
+
+    # association
+    url(r'^associate/(?P<backend>[^/]+)/$', associate,
+        name='socialauth_associate_begin'),
     url(r'^associate/complete/(?P<backend>[^/]+)/$', associate_complete,
         name='socialauth_associate_complete'),
-    url(r'^disconnect/(?P<backend>[^/]+)/$', disconnect, name='socialauth_disconnect'),
-    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$', disconnect,
-        name='socialauth_disconnect_individual'),
+
+    # disconnection
+    url(r'^disconnect/(?P<backend>[^/]+)/$', disconnect,
+        name='socialauth_disconnect'),
+    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$',
+        disconnect, name='socialauth_disconnect_individual'),
 )
index ec5c09e92d543cee243e9dd37af6570766b6c0fc..9f288ed89c96e36564bbc2e72e46d956524cd749 100644 (file)
@@ -1,7 +1,10 @@
 import urlparse
+import logging
 from collections import defaultdict
 
 from django.conf import settings
+from django.db.models import Model
+from django.contrib.contenttypes.models import ContentType
 
 
 def sanitize_log_data(secret, data=None, leave_characters=4):
@@ -68,9 +71,10 @@ def group_backend_by_type(items, key=lambda x: x):
         get_backends, OpenIdAuth, BaseOAuth, BaseOAuth2
 
     result = defaultdict(list)
+    backends = get_backends()
 
     for item in items:
-        backend = get_backends()[key(item)]
+        backend = backends[key(item)]
         if issubclass(backend, OpenIdAuth):
             result['openid'].append(item)
         elif issubclass(backend, BaseOAuth2):
@@ -85,6 +89,41 @@ def setting(name, default=None):
     return getattr(settings, name, default)
 
 
+logger = None
+if not logger:
+    logging.basicConfig()
+    logger = logging.getLogger('SocialAuth')
+    logger.setLevel(logging.DEBUG)
+
+
+def log(level, *args, **kwargs):
+    """Small wrapper around logger functions."""
+    { 'debug': logger.debug,
+      'error': logger.error,
+      'exception': logger.exception,
+      'warn': logger.warn }[level](*args, **kwargs)
+
+
+def model_to_ctype(val):
+    """Converts values that are instance of Model to a dictionary
+    with enough information to retrieve the instance back later."""
+    if isinstance(val, Model):
+        val = {
+            'pk': val.pk,
+            'ctype': ContentType.objects.get_for_model(val).pk
+        }
+    return val
+
+
+def ctype_to_model(val):
+    """Converts back the instance saved by model_to_ctype function."""
+    if isinstance(val, dict) and 'pk' in val and 'ctype' in val:
+        ctype = ContentType.objects.get_for_id(val['ctype'])
+        ModelClass = ctype.model_class()
+        val = ModelClass.objects.get(pk=val['pk'])
+    return val
+
+
 if __name__ == '__main__':
     import doctest
     doctest.testmod()
index d1903a4211691193ff20af2baf9283d7ba5c6d2c..45140ee95621e928252eff691c26600c40ac6a47 100644 (file)
@@ -5,12 +5,8 @@ Notes:
       on third party providers that (if using POST) won't be sending crfs
       token back.
 """
-import logging
-logger = logging.getLogger(__name__)
-
 from functools import wraps
 
-from django.conf import settings
 from django.http import HttpResponseRedirect, HttpResponse, \
                         HttpResponseServerError
 from django.core.urlresolvers import reverse
@@ -20,27 +16,13 @@ from django.contrib import messages
 from django.views.decorators.csrf import csrf_exempt
 
 from social_auth.backends import get_backend
-from social_auth.utils import sanitize_redirect, setting
+from social_auth.utils import sanitize_redirect, setting, log
 
 
 DEFAULT_REDIRECT = setting('SOCIAL_AUTH_LOGIN_REDIRECT_URL') or \
                    setting('LOGIN_REDIRECT_URL')
-NEW_USER_REDIRECT = setting('SOCIAL_AUTH_NEW_USER_REDIRECT_URL')
-NEW_ASSOCIATION_REDIRECT = setting('SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL')
-DISCONNECT_REDIRECT_URL = setting('SOCIAL_AUTH_DISCONNECT_REDIRECT_URL')
-LOGIN_ERROR_URL = setting('LOGIN_ERROR_URL', settings.LOGIN_URL)
-INACTIVE_USER_URL = setting('SOCIAL_AUTH_INACTIVE_USER_URL', LOGIN_ERROR_URL)
-COMPLETE_URL_NAME = setting('SOCIAL_AUTH_COMPLETE_URL_NAME',
-                            'socialauth_complete')
-ASSOCIATE_URL_NAME = setting('SOCIAL_AUTH_ASSOCIATE_URL_NAME',
-                              'socialauth_associate_complete')
-SOCIAL_AUTH_LAST_LOGIN = setting('SOCIAL_AUTH_LAST_LOGIN',
-                                  'social_auth_last_login_backend')
-SESSION_EXPIRATION = setting('SOCIAL_AUTH_SESSION_EXPIRATION', True)
-BACKEND_ERROR_REDIRECT = setting('SOCIAL_AUTH_BACKEND_ERROR_URL',
-                                 LOGIN_ERROR_URL)
-SANITIZE_REDIRECTS = setting('SOCIAL_AUTH_SANITIZE_REDIRECTS', True)
-ERROR_MESSAGE = setting('LOGIN_ERROR_MESSAGE', None)
+LOGIN_ERROR_URL = setting('LOGIN_ERROR_URL', setting('LOGIN_URL'))
+RAISE_EXCEPTIONS = setting('SOCIAL_AUTH_RAISE_EXCEPTIONS', setting('DEBUG'))
 
 
 def dsa_view(redirect_name=None):
@@ -65,23 +47,27 @@ def dsa_view(redirect_name=None):
             try:
                 return func(request, backend, *args, **kwargs)
             except Exception, e:  # some error ocurred
+                if RAISE_EXCEPTIONS:
+                    raise
                 backend_name = backend.AUTH_BACKEND.name
 
-                logger.error(unicode(e), exc_info=True,
-                             extra=dict(request=request))
+                log('error', unicode(e), exc_info=True,
+                    extra=dict(request=request))
 
-                if 'django.contrib.messages' in settings.INSTALLED_APPS:
+                if 'django.contrib.messages' in setting('INSTALLED_APPS'):
                     from django.contrib.messages.api import error
                     error(request, unicode(e), extra_tags=backend_name)
                 else:
-                    logger.warn('Messages framework not in place, some '+
+                    log('warn', 'Messages framework not in place, some '+
                                 'errors have not been shown to the user.')
-                return HttpResponseRedirect(BACKEND_ERROR_REDIRECT)
+
+                url = setting('SOCIAL_AUTH_BACKEND_ERROR_URL', LOGIN_ERROR_URL)
+                return HttpResponseRedirect(url)
         return wrapper
     return dec
 
 
-@dsa_view(COMPLETE_URL_NAME)
+@dsa_view(setting('SOCIAL_AUTH_COMPLETE_URL_NAME', 'socialauth_complete'))
 def auth(request, backend):
     """Start authentication process"""
     return auth_process(request, backend)
@@ -96,7 +82,8 @@ def complete(request, backend, *args, **kwargs):
 
 
 @login_required
-@dsa_view(ASSOCIATE_URL_NAME)
+@dsa_view(setting('SOCIAL_AUTH_ASSOCIATE_URL_NAME',
+                  'socialauth_associate_complete'))
 def associate(request, backend):
     """Authentication starting process"""
     return auth_process(request, backend)
@@ -116,7 +103,9 @@ def associate_complete(request, backend, *args, **kwargs):
     elif isinstance(user, HttpResponse):
         return user
     else:
-        url = NEW_ASSOCIATION_REDIRECT or redirect_value or DEFAULT_REDIRECT
+        url = setting('SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL') or \
+              redirect_value or \
+              DEFAULT_REDIRECT
     return HttpResponseRedirect(url)
 
 
@@ -126,23 +115,21 @@ def disconnect(request, backend, association_id=None):
     """Disconnects given backend from current logged in user."""
     backend.disconnect(request.user, association_id)
     url = request.REQUEST.get(REDIRECT_FIELD_NAME, '') or \
-          DISCONNECT_REDIRECT_URL or \
+          setting('SOCIAL_AUTH_DISCONNECT_REDIRECT_URL') or \
           DEFAULT_REDIRECT
     return HttpResponseRedirect(url)
 
 
 def auth_process(request, backend):
     """Authenticate using social backend"""
-    # Save any defined redirect_to value into session
-    if REDIRECT_FIELD_NAME in request.REQUEST:
-        data = request.POST if request.method == 'POST' else request.GET
-        if REDIRECT_FIELD_NAME in data:
-            # Check and sanitize a user-defined GET/POST redirect_to field value.
-            redirect = data[REDIRECT_FIELD_NAME]
-
-            if SANITIZE_REDIRECTS:
-                redirect = sanitize_redirect(request.get_host(), redirect)
-            request.session[REDIRECT_FIELD_NAME] = redirect or DEFAULT_REDIRECT
+    # Save any defined next value into session
+    data = request.POST if request.method == 'POST' else request.GET
+    if REDIRECT_FIELD_NAME in data:
+        # Check and sanitize a user-defined GET/POST next field value
+        redirect = data[REDIRECT_FIELD_NAME]
+        if setting('SOCIAL_AUTH_SANITIZE_REDIRECTS', True):
+            redirect = sanitize_redirect(request.get_host(), redirect)
+        request.session[REDIRECT_FIELD_NAME] = redirect or DEFAULT_REDIRECT
 
     if backend.uses_redirect:
         return HttpResponseRedirect(backend.auth_url())
@@ -167,7 +154,7 @@ def complete_process(request, backend, *args, **kwargs):
             # in authenticate process
             social_user = user.social_user
 
-            if SESSION_EXPIRATION:
+            if setting('SOCIAL_AUTH_SESSION_EXPIRATION', True):
                 # Set session expiration date if present and not disabled by
                 # setting. Use last social-auth instance for current provider,
                 # users can associate several accounts with a same provider.
@@ -175,19 +162,23 @@ def complete_process(request, backend, *args, **kwargs):
                     request.session.set_expiry(social_user.expiration_delta())
 
             # store last login backend name in session
-            request.session[SOCIAL_AUTH_LAST_LOGIN] = social_user.provider
+            key = setting('SOCIAL_AUTH_LAST_LOGIN',
+                          'social_auth_last_login_backend')
+            request.session[key] = social_user.provider
 
             # Remove possible redirect URL from session, if this is a new
             # account, send him to the new-users-page if defined.
-            if NEW_USER_REDIRECT and getattr(user, 'is_new', False):
-                url = NEW_USER_REDIRECT
+            new_user_redirect = setting('SOCIAL_AUTH_NEW_USER_REDIRECT_URL')
+            if new_user_redirect and getattr(user, 'is_new', False):
+                url = new_user_redirect
             else:
                 url = redirect_value or DEFAULT_REDIRECT
         else:
-            url = INACTIVE_USER_URL or LOGIN_ERROR_URL
+            url = setting('SOCIAL_AUTH_INACTIVE_USER_URL', LOGIN_ERROR_URL)
     else:
-        if ERROR_MESSAGE:
-            messages.error(request, ERROR_MESSAGE)
+        msg = setting('LOGIN_ERROR_MESSAGE', None)
+        if msg:
+            messages.error(request, msg)
         url = LOGIN_ERROR_URL
     return HttpResponseRedirect(url)
 
@@ -196,4 +187,14 @@ def auth_complete(request, backend, user=None, *args, **kwargs):
     """Complete auth process. Return authenticated user or None."""
     if user and not user.is_authenticated():
         user = None
-    return backend.auth_complete(user=user, request=request, *args, **kwargs)
+
+    name = setting('SOCIAL_AUTH_PARTIAL_PIPELINE_KEY', 'partial_pipeline')
+    if request.session.get(name):
+        data = request.session.pop(name)
+        request.session.modified = True
+        idx, args, kwargs = backend.from_session_dict(data, user=user,
+                                                      request=request,
+                                                      *args, **kwargs)
+        return backend.continue_pipeline(pipeline_index=idx, *args, **kwargs)
+    else:
+        return backend.auth_complete(user=user, request=request, *args, **kwargs)