From 6465b04cc673c4979cd6b5f6028dbb0061dbe62d Mon Sep 17 00:00:00 2001 From: Stas Kravets Date: Thu, 16 Feb 2012 14:00:32 +0400 Subject: [PATCH] Merging latest changes from omab --- .gitignore | 3 + README.rst | 348 ++++++++++++++++---- doc/backends/browserid.rst | 14 + doc/backends/github.rst | 8 +- doc/backends/google.rst | 10 + doc/backends/index.rst | 2 + doc/backends/instagram.rst | 18 + doc/configuration.rst | 39 ++- doc/deprecated.rst | 32 ++ doc/index.rst | 3 + doc/intro.rst | 2 + doc/miscellaneous.rst | 3 +- doc/pipeline.rst | 90 +++++ doc/use_cases.rst | 50 +++ example/app/pipeline.py | 14 + example/app/views.py | 18 +- example/local_settings.py.template | 14 +- example/settings.py | 2 + example/templates/base.html | 2 + example/templates/done.html | 31 ++ example/templates/form.html | 15 + example/templates/home.html | 33 ++ example/urls.py | 3 +- social_auth/__init__.py | 2 +- social_auth/backends/__init__.py | 145 ++++++-- social_auth/backends/browserid.py | 90 +++++ social_auth/backends/contrib/dropbox.py | 12 +- social_auth/backends/contrib/flickr.py | 15 +- social_auth/backends/contrib/foursquare.py | 2 - social_auth/backends/contrib/github.py | 32 +- social_auth/backends/contrib/instagram.py | 54 +++ social_auth/backends/contrib/linkedin.py | 9 +- social_auth/backends/contrib/livejournal.py | 2 - social_auth/backends/contrib/orkut.py | 12 +- social_auth/backends/facebook.py | 35 +- social_auth/backends/google.py | 48 +-- social_auth/backends/pipeline/__init__.py | 1 + social_auth/backends/pipeline/associate.py | 4 +- social_auth/backends/pipeline/misc.py | 24 ++ social_auth/backends/pipeline/social.py | 4 +- social_auth/backends/pipeline/user.py | 45 ++- social_auth/backends/twitter.py | 10 +- social_auth/backends/yahoo.py | 5 +- social_auth/context_processors.py | 7 +- social_auth/models.py | 8 +- social_auth/tests/__init__.py | 11 +- social_auth/tests/facebook.py | 7 +- social_auth/tests/google.py | 9 +- social_auth/tests/twitter.py | 9 +- social_auth/urls.py | 21 +- social_auth/utils.py | 41 ++- social_auth/views.py | 97 +++--- 52 files changed, 1207 insertions(+), 308 deletions(-) create mode 100644 doc/backends/browserid.rst create mode 100644 doc/backends/instagram.rst create mode 100644 doc/deprecated.rst create mode 100644 doc/pipeline.rst create mode 100644 doc/use_cases.rst create mode 100644 example/app/pipeline.py create mode 100644 example/templates/form.html create mode 100644 social_auth/backends/browserid.py create mode 100644 social_auth/backends/contrib/instagram.py create mode 100644 social_auth/backends/pipeline/misc.py diff --git a/.gitignore b/.gitignore index bee6585..be0304f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ env/ doc/_build example/templates/script*.html contrib/tests/test_settings.py +*.ipr +*.iws +Django-social-auth.iml diff --git a/README.rst b/README.rst index 007aeb0..e7d1cfb 100644 --- a/README.rst +++ b/README.rst @@ -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 _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:: + + _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//`` 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[^/]+)/$', associate, + name='socialauth_associate_begin'), + url(r'^associate/complete/(?P[^/]+)/$', associate_complete, + name='socialauth_associate_complete'), + url(r'^disconnect/(?P[^/]+)/$', disconnect, + name='socialauth_disconnect'), + url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', + disconnect, name='socialauth_disconnect_individual'), + ) + +2. Include a similar snippet in your page to make BrowserID_ work:: + + + + + +
+ + BrowserID +
+ + + -------------- 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 index 0000000..021ad48 --- /dev/null +++ b/doc/backends/browserid.rst @@ -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 diff --git a/doc/backends/github.rst b/doc/backends/github.rst index ea46e46..293542b 100644 --- a/doc/backends/github.rst +++ b/doc/backends/github.rst @@ -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 diff --git a/doc/backends/google.rst b/doc/backends/google.rst index c3cbcfd..c73cc7e 100644 --- a/doc/backends/google.rst +++ b/doc/backends/google.rst @@ -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 ----- diff --git a/doc/backends/index.rst b/doc/backends/index.rst index 4cb8465..d4dba77 100644 --- a/doc/backends/index.rst +++ b/doc/backends/index.rst @@ -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 index 0000000..2bc4260 --- /dev/null +++ b/doc/backends/instagram.rst @@ -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/ diff --git a/doc/configuration.rst b/doc/configuration.rst index f62a3a8..3b01620 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -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 _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:: + + _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 index 0000000..60d3ff6 --- /dev/null +++ b/doc/deprecated.rst @@ -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 diff --git a/doc/index.rst b/doc/index.rst index 926317c..d9b711e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,10 +15,13 @@ Contents: configuration backends/index + pipeline + deprecated signals contributions testing + use_cases miscellaneous bugs diff --git a/doc/intro.rst b/doc/intro.rst index ea32f25..6d74dea 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -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/ diff --git a/doc/miscellaneous.rst b/doc/miscellaneous.rst index 410e69f..1c1b4ee 100644 --- a/doc/miscellaneous.rst +++ b/doc/miscellaneous.rst @@ -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 index 0000000..776e5d5 --- /dev/null +++ b/doc/pipeline.rst @@ -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//`` 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 index 0000000..2934b86 --- /dev/null +++ b/doc/use_cases.rst @@ -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[^/]+)/$', associate, + name='socialauth_associate_begin'), + url(r'^associate/complete/(?P[^/]+)/$', associate_complete, + name='socialauth_associate_complete'), + url(r'^disconnect/(?P[^/]+)/$', disconnect, + name='socialauth_disconnect'), + url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', + disconnect, name='socialauth_disconnect_individual'), + ) + +2. Include a similar snippet in your page to make BrowserID_ work:: + + + + +
+ + BrowserID +
+ + + diff --git a/example/app/pipeline.py b/example/app/pipeline.py new file mode 100644 index 0000000..0d81b17 --- /dev/null +++ b/example/app/pipeline.py @@ -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/') diff --git a/example/app/views.py b/example/app/views.py index 8c4eaef..4a4c91e 100644 --- a/example/app/views.py +++ b/example/app/views.py @@ -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)) diff --git a/example/local_settings.py.template b/example/local_settings.py.template index 7e928f1..bfbb541 100644 --- a/example/local_settings.py.template +++ b/example/local_settings.py.template @@ -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 diff --git a/example/settings.py b/example/settings.py index 797e7df..a6a26ac 100644 --- a/example/settings.py +++ b/example/settings.py @@ -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', diff --git a/example/templates/base.html b/example/templates/base.html index b528e2f..9e1fd71 100644 --- a/example/templates/base.html +++ b/example/templates/base.html @@ -30,6 +30,8 @@ #valid-badges {position: fixed; right: 10px; bottom: 10px;} #valid-badges p {display: inline;} + + {% block script %}{% endblock %}

Django Social Auth (v{{ version }})

diff --git a/example/templates/done.html b/example/templates/done.html index 04b7b94..edb64c6 100644 --- a/example/templates/done.html +++ b/example/templates/done.html @@ -1,5 +1,10 @@ {% extends "base.html" %} +{% block script %} + + +{% endblock %} + {% block heading %}Logged in!{% endblock %} {% block content %} @@ -85,6 +90,32 @@ {% endfor %} + +

Associate new BrowserID:

+
+ + BrowserID + +
diff --git a/example/templates/form.html b/example/templates/form.html new file mode 100644 index 0000000..fc6e814 --- /dev/null +++ b/example/templates/form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block heading %}User basic form{% endblock %} + +{% block content %} +
+ {% csrf_token %} +

+ + +

+ + +
+{% endblock %} diff --git a/example/templates/home.html b/example/templates/home.html index c1abd7a..edf90f8 100644 --- a/example/templates/home.html +++ b/example/templates/home.html @@ -1,5 +1,10 @@ {% extends "base.html" %} +{% block script %} + + +{% endblock %} + {% block heading %}Login using any of the following methods{% endblock %} {% block content %} @@ -55,6 +60,34 @@
+
+

Login using BrowserID:

+
+ + BrowserID + +
+
+

Login using other authentication systems:

    diff --git a/example/urls.py b/example/urls.py index bcede41..8344d27 100644 --- a/example/urls.py +++ b/example/urls.py @@ -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')), ) diff --git a/social_auth/__init__.py b/social_auth/__init__.py index 7716d9d..40c9af7 100644 --- a/social_auth/__init__.py +++ b/social_auth/__init__.py @@ -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)) diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py index 6a28769..30e84ef 100644 --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@ -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: + _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 _AUTH_EXTRA_ARGUMENTS. + """Return extra arguments needed on auth process, setting is per + backend and defined by: + _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 index 0000000..a8774b5 --- /dev/null +++ b/social_auth/backends/browserid.py @@ -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 +} diff --git a/social_auth/backends/contrib/dropbox.py b/social_auth/backends/contrib/dropbox.py index 0a140fb..33a484b 100644 --- a/social_auth/backends/contrib/dropbox.py +++ b/social_auth/backends/contrib/dropbox.py @@ -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 diff --git a/social_auth/backends/contrib/flickr.py b/social_auth/backends/contrib/flickr.py index b61db38..b6ba55b 100644 --- a/social_auth/backends/contrib/flickr.py +++ b/social_auth/backends/contrib/flickr.py @@ -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""" diff --git a/social_auth/backends/contrib/foursquare.py b/social_auth/backends/contrib/foursquare.py index 9e70d9e..0ba718a 100644 --- a/social_auth/backends/contrib/foursquare.py +++ b/social_auth/backends/contrib/foursquare.py @@ -1,6 +1,4 @@ import urllib -import logging -logger = logging.getLogger(__name__) from django.utils import simplejson diff --git a/social_auth/backends/contrib/github.py b/social_auth/backends/contrib/github.py index 3b13835..b866ca3 100644 --- a/social_auth/backends/contrib/github.py +++ b/social_auth/backends/contrib/github.py @@ -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 index 0000000..e2fba7d --- /dev/null +++ b/social_auth/backends/contrib/instagram.py @@ -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, +} diff --git a/social_auth/backends/contrib/linkedin.py b/social_auth/backends/contrib/linkedin.py index 00ac92b..ae92716 100644 --- a/social_auth/backends/contrib/linkedin.py +++ b/social_auth/backends/contrib/linkedin.py @@ -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) diff --git a/social_auth/backends/contrib/livejournal.py b/social_auth/backends/contrib/livejournal.py index 2e516a0..ce9e538 100644 --- a/social_auth/backends/contrib/livejournal.py +++ b/social_auth/backends/contrib/livejournal.py @@ -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 diff --git a/social_auth/backends/contrib/orkut.py b/social_auth/backends/contrib/orkut.py index c426dab..c7ed4d2 100644 --- a/social_auth/backends/contrib/orkut.py +++ b/social_auth/backends/contrib/orkut.py @@ -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) diff --git a/social_auth/backends/facebook.py b/social_auth/backends/facebook.py index 8e5053e..aca2381 100644 --- a/social_auth/backends/facebook.py +++ b/social_auth/backends/facebook.py @@ -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): diff --git a/social_auth/backends/google.py b/social_auth/backends/google.py index 21b5453..88896ab 100644 --- a/social_auth/backends/google.py +++ b/social_auth/backends/google.py @@ -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""" diff --git a/social_auth/backends/pipeline/__init__.py b/social_auth/backends/pipeline/__init__.py index 6af8113..ae04cde 100644 --- a/social_auth/backends/pipeline/__init__.py +++ b/social_auth/backends/pipeline/__init__.py @@ -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' diff --git a/social_auth/backends/pipeline/associate.py b/social_auth/backends/pipeline/associate.py index 1e260fd..18c9c46 100644 --- a/social_auth/backends/pipeline/associate.py +++ b/social_auth/backends/pipeline/associate.py @@ -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 index 0000000..22b0e19 --- /dev/null +++ b/social_auth/backends/pipeline/misc.py @@ -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 diff --git a/social_auth/backends/pipeline/social.py b/social_auth/backends/pipeline/social.py index 604fb8c..d843616 100644 --- a/social_auth/backends/pipeline/social.py +++ b/social_auth/backends/pipeline/social.py @@ -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 diff --git a/social_auth/backends/pipeline/user.py b/social_auth/backends/pipeline/user.py index eaca1ae..3c9ef22 100644 --- a/social_auth/backends/pipeline/user.py +++ b/social_auth/backends/pipeline/user.py @@ -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'): diff --git a/social_auth/backends/twitter.py b/social_auth/backends/twitter.py index 759fe79..13fc7d1 100644 --- a/social_auth/backends/twitter.py +++ b/social_auth/backends/twitter.py @@ -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 = { diff --git a/social_auth/backends/yahoo.py b/social_auth/backends/yahoo.py index e17577e..65ff401 100644 --- a/social_auth/backends/yahoo.py +++ b/social_auth/backends/yahoo.py @@ -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): diff --git a/social_auth/context_processors.py b/social_auth/context_processors.py index 47bf4c6..1355be1 100644 --- a/social_auth/context_processors.py +++ b/social_auth/context_processors.py @@ -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} diff --git a/social_auth/models.py b/social_auth/models.py index 6d05201..b700329 100644 --- a/social_auth/models.py +++ b/social_auth/models.py @@ -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): diff --git a/social_auth/tests/__init__.py b/social_auth/tests/__init__.py index bc143cc..74ed06f 100644 --- a/social_auth/tests/__init__.py +++ b/social_auth/tests/__init__.py @@ -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 * diff --git a/social_auth/tests/facebook.py b/social_auth/tests/facebook.py index 1eb2735..74d5b47 100644 --- a/social_auth/tests/facebook.py +++ b/social_auth/tests/facebook.py @@ -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) diff --git a/social_auth/tests/google.py b/social_auth/tests/google.py index 9f3cd1d..3beb5a9 100644 --- a/social_auth/tests/google.py +++ b/social_auth/tests/google.py @@ -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'])) diff --git a/social_auth/tests/twitter.py b/social_auth/tests/twitter.py index 6cfc5c5..ee5cc23 100644 --- a/social_auth/tests/twitter.py +++ b/social_auth/tests/twitter.py @@ -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) diff --git a/social_auth/urls.py b/social_auth/urls.py index e1bb193..3d41d3c 100644 --- a/social_auth/urls.py +++ b/social_auth/urls.py @@ -6,12 +6,21 @@ from social_auth.views import auth, complete, associate, associate_complete, \ urlpatterns = patterns('', - url(r'^login/(?P[^/]+)/$', auth, name='socialauth_begin'), - url(r'^complete/(?P[^/]+)/$', complete, name='socialauth_complete'), - url(r'^associate/(?P[^/]+)/$', associate, name='socialauth_associate_begin'), + # authentication + url(r'^login/(?P[^/]+)/$', auth, + name='socialauth_begin'), + url(r'^complete/(?P[^/]+)/$', complete, + name='socialauth_complete'), + + # association + url(r'^associate/(?P[^/]+)/$', associate, + name='socialauth_associate_begin'), url(r'^associate/complete/(?P[^/]+)/$', associate_complete, name='socialauth_associate_complete'), - url(r'^disconnect/(?P[^/]+)/$', disconnect, name='socialauth_disconnect'), - url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', disconnect, - name='socialauth_disconnect_individual'), + + # disconnection + url(r'^disconnect/(?P[^/]+)/$', disconnect, + name='socialauth_disconnect'), + url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', + disconnect, name='socialauth_disconnect_individual'), ) diff --git a/social_auth/utils.py b/social_auth/utils.py index ec5c09e..9f288ed 100644 --- a/social_auth/utils.py +++ b/social_auth/utils.py @@ -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() diff --git a/social_auth/views.py b/social_auth/views.py index d1903a4..45140ee 100644 --- a/social_auth/views.py +++ b/social_auth/views.py @@ -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) -- 2.39.5