doc/_build
example/templates/script*.html
contrib/tests/test_settings.py
+*.ipr
+*.iws
+Django-social-auth.iml
-==================
Django Social Auth
==================
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:
- 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_
- Several backends demands application registration on their corresponding
sites.
-
-------------
Installation
------------
$ cd django-social-auth
$ sudo python setup.py install
-
--------------
Configuration
-------------
+
- Add social_auth to ``PYTHONPATH`` and installed applications::
INSTALLED_APPS = (
'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',
'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',
)
DROPBOX_API_SECRET = ''
FLICKR_APP_ID = ''
FLICKR_API_SECRET = ''
+ INSTAGRAM_CLIENT_ID = ''
+ INSTAGRAM_CLIENT_SECRET = ''
- Setup login URLs::
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::
<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::
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
-------------
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`_,
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.
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,
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`_:
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`_:
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.
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
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.
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::
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
DROPBOX_APP_ID = ''
DROPBOX_API_SECRET = ''
-
-------
Flickr
-------
+^^^^^^
+
Flickr uses OAuth v1.0 for authentication.
- Register a new application at the `Flickr App Garden`_, and
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
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):
- Flickr support
- Provider name context processor
-----------
+- r4vi_ (Ravi Kotecha)
+
+ - Instagram support
+
Copyrights
----------
+
Base work is copyrighted by:
- django-twitter-oauth::
.. _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
.. _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/
--- /dev/null
+---------
+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
======
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
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
-----
facebook
linkedin
github
+ browserid
+ instagram
--- /dev/null
+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/
'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',
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::
<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::
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
--- /dev/null
+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
configuration
backends/index
+ pipeline
+ deprecated
signals
contributions
testing
+ use_cases
miscellaneous
bugs
- 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/
.. _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/
+
--- /dev/null
+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
--- /dev/null
+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>
--- /dev/null
+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/')
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"""
@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):
"""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))
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
'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',
#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>
{% 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>
--- /dev/null
+{% 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 %}
{% 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>
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()
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')),
)
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))
(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
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
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
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)
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"""
"""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):
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()),
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' % \
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"""
@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):
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):
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.
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
"""
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
+}
--- /dev/null
+"""
+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
+}
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
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"""
@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
"""
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
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"""
import urllib
-import logging
-logger = logging.getLogger(__name__)
from django.utils import simplejson
"""
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
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"""
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'
@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
--- /dev/null
+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,
+}
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
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)
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
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
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',
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)
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
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?'
"""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', '')}
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"""
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):
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]
@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):
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'
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):
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):
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"""
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):
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)
# 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'
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"""
from django.conf import settings
from social_auth.models import User
+from social_auth.backends import get_backend, PIPELINE
USERNAME = 'username'
-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
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
--- /dev/null
+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
-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
"""
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
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
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.
"""
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}
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,
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'):
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
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 = {
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):
"""
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}
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
# 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
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):
-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 *
import re
-from django.conf import settings
-
+from social_auth.utils import setting
from social_auth.tests.base import SocialAuthTestsCase, FormParserByID
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)
import re
-from django.conf import settings
-
+from social_auth.utils import setting
from social_auth.tests.base import SocialAuthTestsCase, FormParserByID, \
FormParser, RefreshParser
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)
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']))
-from django.conf import settings
-
+from social_auth.utils import setting
from social_auth.tests.base import SocialAuthTestsCase, FormParserByID, \
RefreshParser
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)
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)
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'),
)
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):
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):
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()
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
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):
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)
@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)
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)
"""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())
# 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.
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)
"""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)