├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_stormpath ├── __init__.py ├── admin.py ├── backends.py ├── forms.py ├── helpers.py ├── id_site.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── create_social_directory.py │ │ └── sync_accounts_from_stormpath.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150826_1154.py │ ├── 0003_auto_20160426_1425.py │ └── __init__.py ├── models.py ├── social.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_stormpathuser_is_verified.py │ └── __init__.py ├── urls.py └── views.py ├── docs ├── Makefile ├── assets │ └── images │ │ └── stormpath-logo.png ├── conf.py ├── django_stormpath.rst ├── index.rst └── make.bat ├── requirements.txt ├── setup.py └── testproject ├── manage.py ├── testapp ├── __init__.py ├── models.py ├── templates │ └── testapp │ │ └── index.html ├── tests.py └── views.py └── testproject ├── __init__.py ├── settings.py ├── test_settings.py ├── urls.py ├── utils.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.xml 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | 10 | # Installer logs 11 | pip-log.txt 12 | 13 | # Unit test / coverage reports 14 | .coverage 15 | .tox 16 | 17 | # tools 18 | .idea 19 | *.iml 20 | 21 | # Distutils 22 | MANIFEST 23 | 24 | # Other 25 | .DS_Store 26 | *.swp 27 | 28 | # Sphinx 29 | /docs/_build/ 30 | /docs/_static/ 31 | /docs/_templates/ 32 | 33 | /testproject/.env 34 | /testproject/dev.db 35 | /testproject/htmlcov 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - '2.7' 5 | - '3.4' 6 | - '3.5' 7 | - pypy 8 | install: 9 | - pip install -e .[test] 10 | - pip install -q Django==$DJANGO 11 | script: 12 | - python setup.py test 13 | after_success: 14 | - cd testproject && coverage xml 15 | - coveralls 16 | - python-codacy-coverage -r coverage.xml 17 | env: 18 | matrix: 19 | - DJANGO=1.8 20 | - DJANGO=1.9 21 | - DJANGO=1.10 22 | global: 23 | - GOOGLE_CLIENT_ID=xxx 24 | - GOOGLE_CLIENT_SECRET=xxx 25 | - FACEBOOK_CLIENT_ID=xxx 26 | - FACEBOOK_CLIENT_SECRET=xxx 27 | - GITHUB_CLIENT_ID=xxx 28 | - GITHUB_CLIENT_SECRET=xxx 29 | - LINKEDIN_CLIENT_ID=xxx 30 | - LINKEDIN_CLIENT_SECRET=xxx 31 | - secure: J24jJ8Sp6e0At5raohFTqx/g2gtBkdTHQ4kk1nvbe/aGrHJbwJNliXEwNftOLm39eyoUyzRx6qXliEGf7Pk7Xwt4xtfcaEq0hnzpdw3CREQOi0h8udY5UIOFGabGPyTjN5lNJTGGINcGl3pjqd1+JCDfnM/nqbTfPQ3wRhKOlbM= 32 | - secure: WKDRKgGrvuVSzArPkrihdmkQEC9F5NqS4Kf3nC5xFCFuM+HcnhhAXAYk/zaFv7vliy1bm2c6FMbTTtnH4r3qBS7QV0Cj3vPsrZxTGuFsME80/g5eRc30laBlyBV8JiFVPIX9GeoxUiMivuki+ja0MdEvUGP8ERgV1tQcNLEPSh0= 33 | - secure: ST9PPcRbrRtS/xeWamdabc+0Q7MpfGd9X0Ewq5MONvfzX1p9Z/6+dZRAmxUw9RJopjBWKSdfkC8rrkPbfFQw9++oRW9YCghHjlq8W7sJU7cN/Fn8BJgOJsxzoXdjOMVqI5Ta7PLHsradcW+VGeIj/5uFPA+amJwv/OW0gUDBrgg= 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst django_stormpath/*.py 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Stormpath is Joining Okta 3 | ========================== 4 | 5 | We are incredibly excited to announce that `Stormpath is joining forces with Okta `_. Please visit `the Migration FAQs `_ for a detailed look at what this means for Stormpath users. 6 | 7 | We're available to answer all questions at `support@stormpath.com `_. 8 | 9 | django-stormpath 10 | ================ 11 | 12 | Simple, scalable, user authentication for Django (powered by `Stormpath `_). 13 | 14 | .. image:: https://img.shields.io/pypi/v/django-stormpath.svg 15 | :alt: stormpath Release 16 | :target: https://pypi.python.org/pypi/django-stormpath 17 | 18 | .. image:: https://img.shields.io/pypi/dm/django-stormpath.svg 19 | :alt: stormpath Downloads 20 | :target: https://pypi.python.org/pypi/django-stormpath 21 | 22 | .. image:: https://api.codacy.com/project/badge/Grade/1c14c778d4e24604b48814144db18024 23 | :alt: stormpath-django Code Quality 24 | :target: https://www.codacy.com/app/r/stormpath-django 25 | 26 | .. image:: https://img.shields.io/travis/stormpath/stormpath-django.svg 27 | :alt: stormpath-django Build 28 | :target: https://travis-ci.org/stormpath/stormpath-django 29 | 30 | .. image:: https://coveralls.io/repos/github/stormpath/stormpath-django/badge.svg?branch=master 31 | :alt: stormpath-django Coverage 32 | :target: https://coveralls.io/github/stormpath/stormpath-django?branch=master 33 | 34 | .. note:: 35 | This is a beta release, if you run into any issues, please file a report on 36 | our `Issue Tracker `_. 37 | 38 | 39 | Meta 40 | ---- 41 | 42 | This library provides user account storage, authentication, and authorization 43 | with `Stormpath `_. 44 | 45 | This library works with Python 2.7.x and Python 3.3+. It supports Django versions 1.6+. 46 | 47 | .. note:: 48 | This library will NOT work on Google App Engine due to incompatibilities 49 | with the 50 | `requests `_ 51 | package :( 52 | 53 | 54 | Installation 55 | ------------ 56 | 57 | To install the library, you'll need to use `pip `_: 58 | 59 | .. code-block:: console 60 | 61 | $ pip install django-stormpath 62 | 63 | 64 | How it Works 65 | ------------ 66 | 67 | django-stormpath provides an ``AUTHENTICATION_BACKEND`` which is used to 68 | communicate with the Stormpath REST API. 69 | 70 | When a user tries to log in, and Stormpath is used as the authentication 71 | backend, django-stormpath always asks the Stormpath service if the user's 72 | credentials (*username or email and password*) are correct. Password hashes are 73 | always stored in Stormpath, and not locally. 74 | 75 | If a user's credentials are valid, there are two possible scenarios: 76 | 77 | 1. User doesn't exist in Django's database (*PostgreSQL, MySQL, Oracle etc.*). 78 | In this case, a user will be created in the local Django user database with 79 | their username, password, email, first name, and last name identical to the 80 | Stormpath user's. Then this user will be authenticated. 81 | 82 | 2. User exists in Django's database. In this case, if a user's information has 83 | changed on Stormpath, the Django user's fields are updated accordingly. 84 | After this, the user will be authenticated. 85 | 86 | .. note:: 87 | An account on Stormpath can be disabled, enabled, locked and unverified. 88 | When a user is created or updated, the ``is_active`` field is set to 89 | ``True`` if the Stormpath account is enabled and ``False`` otherwise. 90 | For a Stormpath user to be able to log into the Django admin interface 91 | it must specify the ``is_superuser`` and ``is_staff`` properties in the 92 | Stormpath Account's customData resource. 93 | 94 | 95 | Usage 96 | ----- 97 | 98 | First, you need to add ``django_stormpath`` to your ``INSTALLED_APPS`` setting 99 | in ``settings.py``: 100 | 101 | .. code-block:: python 102 | 103 | INSTALLED_APPS = ( 104 | # ..., 105 | 'django_stormpath', 106 | ) 107 | 108 | Next, you need to add the Stormpath backend into ``AUTHENTICATION_BACKENDS``: 109 | 110 | .. code-block:: python 111 | 112 | AUTHENTICATION_BACKENDS = ( 113 | # ... 114 | 'django_stormpath.backends.StormpathBackend', 115 | ) 116 | 117 | After that's done, you'll also need to tell Django to use Stormpath's user 118 | model: 119 | 120 | .. code-block:: python 121 | 122 | AUTH_USER_MODEL = 'django_stormpath.StormpathUser' 123 | 124 | You can read more about how Django's custom user model works `here _`. 125 | 126 | Lastly, you need to specify your Stormpath credentials: your API key and secret, 127 | as well as your Stormpath Application URL. 128 | 129 | For more information about these things, please see the official 130 | `guide `_. 131 | 132 | To specify your Stormpath credentials, you'll need to add the following settings 133 | to your ``settings.py``: 134 | 135 | .. code-block:: python 136 | 137 | STORMPATH_ID = 'yourApiKeyId' 138 | STORMPATH_SECRET = 'yourApiKeySecret' 139 | STORMPATH_APPLICATION = 'https://api.stormpath.com/v1/applications/YOUR_APP_UID_HERE' 140 | 141 | Once this is done, you're ready to get started! The next thing you need to do 142 | is to sync your database and apply any migrations: 143 | 144 | .. code-block:: console 145 | 146 | $ python manage.py migrate 147 | 148 | And that's it! You're now ready to get started =) 149 | 150 | 151 | Example: Creating a User 152 | ------------------------ 153 | 154 | To pragmatically create a user, you can use the following code: 155 | 156 | .. code-block:: python 157 | 158 | from django.contrib.auth import get_user_model 159 | 160 | UserModel = get_user_model() 161 | UserModel.objects.create( 162 | email = 'john.doe@example.com', 163 | given_name = 'John', 164 | surname = 'Doe', 165 | password = 'password123!' 166 | ) 167 | 168 | The above example just calls the ``create_user`` method: 169 | 170 | .. code-block:: python 171 | 172 | UserModel.objects.create_user('john.doe@example.com', 'John', 'Doe', 'Password123!') 173 | 174 | To create a super user, you can use ``manage.py``: 175 | 176 | .. code-block:: console 177 | 178 | $ python manage.py createsuperuser --username=joe --email=joe@example.com 179 | 180 | This will set ``is_admin``, ``is_staff`` and ``is_superuser`` to ``True`` on 181 | the newly created user. All extra parameters like the aforementioned flags are 182 | saved on Stormpath in the Accounts customData Resource and can be inspected 183 | outside of Django. This just calls the ``UserModel.objects.create_superuser`` method 184 | behind the scenes. 185 | 186 | Once you're all set up you can use the ``StormpathUser`` model just as you would the normal 187 | django user model to form relationships within your models: 188 | 189 | class Book(models.Model): 190 | author = models.ForeignKey(settings.AUTH_USER_MODEL) 191 | 192 | 193 | .. note:: 194 | When doing the initial ``migrate`` call (or ``manage.py createsuperuser``) 195 | an Account is also created on Stormpath. Every time the ``save`` method 196 | is called on the UserModel instance it is saved/updated on Stormpath as 197 | well. This includes working with the Django built-in admin interface. 198 | 199 | 200 | ID Site 201 | ------- 202 | 203 | If you'd like to not worry about building your own registration and login 204 | screens at all, you can use Stormpath's new `ID site feature 205 | `_. This is a hosted login 206 | subdomain which handles authentication for you automatically. 207 | 208 | To make this work in Django, you need to specify a few settings: 209 | 210 | .. code-block:: python 211 | 212 | AUTHENTICATION_BACKENDS = ( 213 | # ... 214 | 'django_stormpath.backends.StormpathIdSiteBackend', 215 | ) 216 | 217 | # This should be set to the same URI you've specified in your Stormpath ID 218 | # Site dashboard. NOTE: This URL must be *exactly* the same as the one in 219 | # your Stormpath ID Site dashboard (under the Authorized Redirect URLs input 220 | # box). 221 | STORMPATH_ID_SITE_CALLBACK_URI = 'http://localhost:8000/handle-callback/stormpath/ 222 | 223 | # The URL you'd like to redirect users to after they've successfully logged 224 | # into their account. 225 | LOGIN_REDIRECT_URL = '/redirect/here' 226 | 227 | Lastly, you've got to include some URLs in your main ``urls.py`` as well: 228 | 229 | .. code-block:: python 230 | 231 | url(r'', include('django_stormpath.urls')), 232 | 233 | An example of how to use the available URL mappings can be found `here 234 | `_. 235 | 236 | 237 | Social Login 238 | ------------ 239 | 240 | Django Stormpath supports social login as well. Currently supported Providers are: Google, Github, Linkedin and Facebook. 241 | First thing that you need to do is add `StormpathSocialBackend` to the list of allowed authentication backends 242 | in your settings file: 243 | 244 | .. code-block:: python 245 | 246 | AUTHENTICATION_BACKENDS = ( 247 | # ... 248 | 'django_stormpath.backends.StormpathSocialBackend', 249 | ) 250 | 251 | After that you can enable each provider with the following settings: 252 | 253 | .. code-block:: python 254 | 255 | STORMPATH_ENABLE_GOOGLE = True 256 | STORMPATH_ENABLE_FACEBOOK = True 257 | STORMPATH_ENABLE_GITHUB = True 258 | STORMPATH_ENABLE_LINKEDIN = True 259 | 260 | STORMPATH_SOCIAL = { 261 | 'GOOGLE': { 262 | 'client_id': os.environ['GOOGLE_CLIENT_ID'], 263 | 'client_secret': os.environ['GOOGLE_CLIENT_SECRET'], 264 | }, 265 | 'FACEBOOK': { 266 | 'client_id': os.environ['FACEBOOK_CLIENT_ID'], 267 | 'client_secret': os.environ['FACEBOOK_CLIENT_SECRET'] 268 | }, 269 | 'GITHUB': { 270 | 'client_id': os.environ['GITHUB_CLIENT_ID'], 271 | 'client_secret': os.environ['GITHUB_CLIENT_SECRET'] 272 | }, 273 | 'LINKEDIN': { 274 | 'client_id': os.environ['LINKEDIN_CLIENT_ID'], 275 | 'client_secret': os.environ['LINKEDIN_CLIENT_SECRET'] 276 | }, 277 | } 278 | 279 | 280 | And that's it! Now if you navigate to "https://yourdjangoapp.com/social-login/google/" for each provider respectively, 281 | you will be redirected to that provider for authentication. If you are authenticated succesffully you will be redirected back 282 | to your django app and logged in automatically. Stormpath django also creates a directory for each social provider automatically 283 | so you don't need to worry about it. 284 | 285 | .. note:: 286 | Please note that the callback URL's for each provider are listed in django stormpath's urls.py file. 287 | You will need to use these callback urls and set them as redirect URI's when configuring each provider 288 | in their respecive dashboards. For intance the callback URL for Google is: "https://yourdjangoapp.com/social-login/google/callback". 289 | 290 | .. note:: 291 | Note that for OAuth2 to work we need to be using HTTPS. 292 | For django to work correctly with HTTPS please set the following settings: 293 | 294 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 295 | SESSION_COOKIE_SECURE = True 296 | CSRF_COOKIE_SECURE = True 297 | 298 | 299 | Caching 300 | ------- 301 | 302 | The best kind of websites are fast websites. ``Django-Stormpath`` includes 303 | built-in support for caching. You can currently use either: 304 | 305 | - A local memory cache (*default*). 306 | - A `memcached `_ cache. 307 | - A `redis `_ cache. 308 | 309 | All can be easily configured using configuration variables. 310 | 311 | There are several configuration settings you can specify to control caching 312 | behavior. You need to add the ``STORMPATH_CACHE_OPTIONS`` to your Django 313 | project's settings file. 314 | 315 | Here's an example which shows how to enable caching with redis:: 316 | 317 | from stormpath.cache.redis_store import RedisStore 318 | 319 | STORMPATH_CACHE_OPTIONS = { 320 | 'store': RedisStore, 321 | 'store_opts': { 322 | 'host': 'localhost', 323 | 'port': 6379 324 | } 325 | } 326 | 327 | Here's an example which shows how to enable caching with memcached:: 328 | 329 | from stormpath.cache.memcached_store import MemcachedStore 330 | 331 | STORMPATH_CACHE_OPTIONS = { 332 | 'store': MemcachedStore, 333 | 'store_opts': { 334 | 'host': 'localhost', 335 | 'port': 11211 336 | } 337 | } 338 | 339 | If no cache is specified, the default, ``MemoryStore``, is used. This will 340 | cache all resources in local memory. 341 | 342 | For a full list of options available for each cache backend, please see the 343 | official `Caching Docs `_ 344 | in our Python library. 345 | 346 | 347 | Copyright and License 348 | --------------------- 349 | 350 | Copyright © 2014 Stormpath, inc. You may use and/or modify this library 351 | under the terms of Apache License version 2.0. Please see the 352 | `LICENSE `_ 353 | file for details. 354 | 355 | 356 | Change Log 357 | ---------- 358 | 359 | All library changes, in descending order. 360 | 361 | 362 | Version 1.1.0 363 | ************* 364 | 365 | **Released August 12, 2016.** 366 | 367 | - Dropping Python 3.3 support since Django doesn't support it in 1.10. 368 | - Supporting Python 3.5. 369 | - Dropping support for Django 1.6 / 1.7, since they are EOL by Django. 370 | - Fixing broken tests. 371 | - Adding coverage reporting. 372 | - Renaming management task ``sync_account`` -> ``sync_accounts_from_stormpath``. 373 | - Adding option to ``sync_accounts_from_stormpath`` to sync Stormpath Groups in 374 | addition to accounts. 375 | - Various code cleanup. 376 | 377 | 378 | Version 1.0.7 379 | ************* 380 | 381 | **Released August 10, 2016.** 382 | 383 | - Supporting new Django management command: ``sync_accounts```. This will 384 | synchronize all remote Stormpath Accounts with the local Django database. 385 | This helps developers keep things in sync (like in the Django admin). Thanks 386 | to `Peter Novotnak `_ for the PR! 387 | - Fixing some old, incorrect setup docs. 388 | 389 | 390 | Version 1.0.6 391 | ************* 392 | 393 | **Released February 18, 2016.** 394 | 395 | - Version bump for Stormpath dependency. 396 | - Fixing error handling issues which now work better with underlying Python 397 | library fixes. 398 | - Refactoring our ID site callback handling to use our new Python bindings. 399 | 400 | 401 | Version 1.0.5 402 | ************* 403 | 404 | **Released November 13, 2015.** 405 | 406 | - Version bump for Stormpath dependency. 407 | 408 | 409 | Version 1.0.4 410 | ************* 411 | 412 | **Released November 2, 2015.** 413 | 414 | - Removing Python 3.2 support. Nobody uses it (*buggy release*). 415 | - Raising a proper ``DoesNotExist`` exception when a Resource 404 is returned 416 | from the Stormpath API. 417 | - Updating docs to reflect what versions of Django we support (*1.6+*). 418 | - Allowing users to update a user's password by working around the data 419 | mirroring issue with Django. 420 | - Supporting the ``check_password`` Django API, thanks to an awesome pull 421 | request from `smcoll `_. 422 | - Fixing email verification bug. 423 | - Adding get-or-create support. 424 | - Updating stormpath dependency. 425 | 426 | 427 | Version 1.0.3 428 | ************* 429 | 430 | **Released on June 18, 2015.** 431 | 432 | - Updating ID site docs slightly. 433 | - Fixing Travis CI builds. 434 | - Upgrading to the latest Stormpath release. 435 | 436 | 437 | Version 1.0.2 438 | ************* 439 | 440 | **Released on May 12, 2015.** 441 | 442 | - Improving Travis CI builds so that tests are run against Django 1.6.x, 1.7.x, 443 | and 1.8.x. This will help flush out Django version issues (*hopefully!*). 444 | - Fixing old migration issue. This should make all new ``migrate`` commands run 445 | successfully regardless of database used. 446 | - Supporting ``User.first_name`` and ``User.last_name`` per Django's 447 | conventions. This makes our user model play nice with third party Django apps 448 | =) 449 | 450 | 451 | Version 1.0.1 452 | ************* 453 | 454 | **Released on April 30, 2015.** 455 | 456 | - Adding missing migrations. This fixes issues when running ``migrate`` on a 457 | new Postgres database. 458 | - Making the built-in ``delete()`` method remove both copies of the account. 459 | 460 | 461 | Version 1.0.0 462 | ************* 463 | 464 | **Released on April 18, 2015.** 465 | 466 | - Fixing issue with ``StormpathPermissionsMixin`` by replacing it with the 467 | built-in ``PermissionsMixin`` that Django provides. Thanks again, 468 | `@davidmarquis `_! 469 | - The above change is a **breaking** change -- so users of earlier versions of 470 | django-stormpath are encouraged to stay on their current release unless they 471 | want to manually handle the database migrations. This breakage is *very rare* 472 | for our libraries, but was necessary in this case to fix the underlying 473 | library issues. 474 | - Updating broken test case for the new release. 475 | 476 | 477 | Version 0.0.7 478 | ************* 479 | 480 | **Released on April 15, 2015.** 481 | 482 | - Fixing documentation issue in the README -- we had an incorrect code sample 483 | setting up urlpatterns. Thanks `@espenak `_ for 484 | the find! 485 | - Adding a `StormpathUserManager.delete()` method. This makes it possible to 486 | 'cleanly' delete users from both Django and Stormpath. 487 | - Fixing Group permission editing. Thanks `@davidmarquis `_! 488 | - Fixing bug with maintaining the username field when editing user objects. 489 | Thanks again, `@davidmarquis `_! 490 | - Adding in missing dependency: ``requests_oauthlib``. This is required for our 491 | ID site functionality to work, but was missing. 492 | 493 | 494 | Version 0.0.6 495 | ************* 496 | 497 | **Released on February 11, 2015.** 498 | 499 | - PEP-8 fixing imports, and making things python 3 compatible (thanks 500 | @rtrajano)! 501 | 502 | 503 | Version 0.0.5 504 | ************* 505 | 506 | **Released on February 5, 2015.** 507 | 508 | - Adding support for social login. 509 | - Various test fixes. 510 | - PEP-8. 511 | 512 | 513 | Version 0.0.4 514 | ************* 515 | 516 | **Released on January 19, 2015.** 517 | 518 | - Fixing incompatible arguments being passed from django-rest-framework-jwt to 519 | ``StormpathBackend.authenticate()``. 520 | - Changing unexpected behaviors (*no return value*) of 521 | ``StormpathuserManager.create()``. 522 | 523 | All fixes thanks to `@skolsuper `_! 524 | 525 | 526 | Version 0.0.3 527 | ************* 528 | 529 | **Released on December 9, 2014.** 530 | 531 | - Adding cache support. 532 | - Fixing docs. 533 | - Adding docs on caching. 534 | - Adding support for ID site. 535 | 536 | 537 | Version 0.0.2 538 | ************* 539 | 540 | **Released on November 26, 2014.** 541 | 542 | - Fixing README stuff :( 543 | 544 | 545 | Version 0.0.1 546 | ************* 547 | 548 | **Released on November 26, 2014.** 549 | 550 | - First release! 551 | -------------------------------------------------------------------------------- /django_stormpath/__init__.py: -------------------------------------------------------------------------------- 1 | __version_info__ = ('1', '1', '0') 2 | __version__ = '.'.join(__version_info__) 3 | __short_version__ = '.'.join(__version_info__) 4 | __author__ = 'Stormpath, Inc.' 5 | __license__ = 'Apache' 6 | __copyright__ = '(c) 2012 - 2015 Stormpath, Inc.' 7 | -------------------------------------------------------------------------------- /django_stormpath/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .models import StormpathUser 5 | from .forms import StormpathUserCreationForm, StormpathUserChangeForm 6 | 7 | 8 | class StormpathUserAdmin(UserAdmin): 9 | # Set the add/modify forms 10 | add_form = StormpathUserCreationForm 11 | form = StormpathUserChangeForm 12 | # The fields to be used in displaying the User model. 13 | # These override the definitions on the base UserAdmin 14 | # that reference specific fields on auth.User. 15 | list_display = ('email', 'is_staff', 'given_name', 'surname') 16 | list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') 17 | search_fields = ('email', 'given_name', 'surname') 18 | ordering = ('email',) 19 | filter_horizontal = ('groups', 'user_permissions',) 20 | fieldsets = ( 21 | (None, {'fields': ('email', 'password')}), 22 | ('Personal info', {'fields': ('given_name', 'surname')}), 23 | ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',)}), 24 | ('Important dates', {'fields': ('last_login',)}), 25 | ) 26 | add_fieldsets = ( 27 | (None, {'classes': ('wide',), 28 | 'fields': ('given_name', 'surname', 'email', 'password1', 'password2')}), 29 | ) 30 | 31 | # Register the new CustomUserAdmin 32 | admin.site.register(StormpathUser, StormpathUserAdmin) 33 | -------------------------------------------------------------------------------- /django_stormpath/backends.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.db.models import Q 5 | from django.contrib.auth.backends import ModelBackend 6 | from django.contrib.auth.models import Group 7 | from stormpath.error import Error 8 | 9 | 10 | log = getLogger(__name__) 11 | 12 | 13 | def get_application(): 14 | """Helper function. Needed for easier testing""" 15 | from .models import APPLICATION 16 | return APPLICATION 17 | 18 | 19 | class StormpathBackend(ModelBackend): 20 | """Authenticate with STORMPATH_setting in settings.py""" 21 | 22 | def _stormpath_authenticate(self, username, password): 23 | """Check if Stormpath authentication works 24 | 25 | :param username: Can be actual username or email 26 | :param password: Account password 27 | 28 | Returns an account object if successful or None otherwise. 29 | """ 30 | APPLICATION = get_application() 31 | try: 32 | result = APPLICATION.authenticate_account(username, password) 33 | return result.account 34 | except Error as e: 35 | log.debug(e) 36 | return None 37 | 38 | def _get_group_difference(self, sp_groups): 39 | """Helper method for gettings the groups that 40 | are present in the local db but not on stormpath 41 | and the other way around.""" 42 | db_groups = set(Group.objects.all().values_list('name', flat=True)) 43 | missing_from_db = set(sp_groups).difference(db_groups) 44 | missing_from_sp = db_groups.difference(sp_groups) 45 | 46 | return (missing_from_db, missing_from_sp) 47 | 48 | def _mirror_groups_from_stormpath(self): 49 | """Helper method for saving to the local db groups 50 | that are missing but are on Stormpath""" 51 | APPLICATION = get_application() 52 | sp_groups = [g.name for g in APPLICATION.groups] 53 | missing_from_db, missing_from_sp = self._get_group_difference(sp_groups) 54 | 55 | if missing_from_db: 56 | groups_to_create = [] 57 | 58 | for g_name in missing_from_db: 59 | groups_to_create.append(Group(name=g_name)) 60 | 61 | Group.objects.bulk_create(groups_to_create) 62 | 63 | def _create_or_get_user(self, account): 64 | UserModel = get_user_model() 65 | 66 | try: 67 | user = UserModel.objects.get(Q(username=account.username) | Q(email=account.email)) 68 | user._mirror_data_from_stormpath_account(account) 69 | self._mirror_groups_from_stormpath() 70 | users_sp_groups = [g.name for g in account.groups] 71 | user.groups = Group.objects.filter(name__in=users_sp_groups) 72 | user._save_db_only() 73 | 74 | return user 75 | except UserModel.DoesNotExist: 76 | user = UserModel() 77 | user._mirror_data_from_stormpath_account(account) 78 | self._mirror_groups_from_stormpath() 79 | user._save_db_only() 80 | users_sp_groups = [g.name for g in account.groups] 81 | user.groups = Group.objects.filter(name__in=users_sp_groups) 82 | user._save_db_only() 83 | 84 | return user 85 | 86 | def authenticate(self, username=None, password=None, **kwargs): 87 | """The authenticate method takes credentials as keyword arguments, 88 | usually username/email and password. 89 | 90 | Returns a user model if the Stormpath authentication was successful or 91 | None otherwise. It expects three variable to be defined in Django 92 | settings: \n 93 | STORMPATH_ID = "apiKeyId" \n 94 | STORMPATH_SECRET = "apiKeySecret" \n 95 | STORMPATH_APPLICATION = 96 | "https://api.stormpath.com/v1/applications/APP_UID" 97 | """ 98 | if username is None: 99 | UserModel = get_user_model() 100 | username = kwargs.get(UserModel.USERNAME_FIELD) 101 | 102 | account = self._stormpath_authenticate(username, password) 103 | if account is None: 104 | return None 105 | 106 | return self._create_or_get_user(account) 107 | 108 | 109 | class StormpathIdSiteBackend(StormpathBackend): 110 | """Used for authenticating with ID Site""" 111 | 112 | def authenticate(self, account=None): 113 | if account is None: 114 | return None 115 | 116 | return self._create_or_get_user(account) 117 | 118 | 119 | class StormpathSocialBackend(StormpathIdSiteBackend): 120 | """Used for authenticating with GOOGLE/FACEBOOK/others""" 121 | pass 122 | -------------------------------------------------------------------------------- /django_stormpath/forms.py: -------------------------------------------------------------------------------- 1 | """Example forms that can be used for CRUD actions in applications. 2 | """ 3 | 4 | from django import forms 5 | from django.contrib.auth import get_user_model 6 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 7 | 8 | from stormpath.error import Error 9 | 10 | from .models import APPLICATION 11 | 12 | 13 | class StormpathUserCreationForm(forms.ModelForm): 14 | """User creation form. 15 | 16 | Creates a new user on Stormpath and locally. 17 | """ 18 | 19 | password1 = forms.CharField(label='Password', widget=forms.PasswordInput) 20 | password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) 21 | 22 | class Meta: 23 | model = get_user_model() 24 | fields = ('username', 'email', 'given_name', 'surname', 'password1', 'password2') 25 | 26 | def clean_password2(self): 27 | """Check if passwords match and are valid.""" 28 | password1 = self.cleaned_data.get('password1') 29 | password2 = self.cleaned_data.get('password2') 30 | 31 | try: 32 | directory = APPLICATION.default_account_store_mapping.account_store 33 | directory.password_policy.strength.validate_password(password2) 34 | except ValueError as e: 35 | raise forms.ValidationError(str(e)) 36 | 37 | if password1 != password2: 38 | msg = "Passwords don't match." 39 | raise forms.ValidationError(msg) 40 | 41 | return password2 42 | 43 | def clean_username(self): 44 | """Check if username exists on Stormpath. 45 | 46 | We don't want the form to validate if a user with the same username 47 | exists on Stormpath. We ignore the status of the local user because we 48 | delete ther user on save to keep in sync with Stormpath. 49 | """ 50 | try: 51 | accounts = APPLICATION.accounts.search({'username': self.cleaned_data['username']}) 52 | if len(accounts): 53 | msg = "User with that username already exists." 54 | raise forms.ValidationError(msg) 55 | except Error as e: 56 | raise forms.ValidationError(str(e)) 57 | 58 | return self.cleaned_data['username'] 59 | 60 | def clean_email(self): 61 | """Check if email exists on Stormpath. 62 | 63 | The email address is unique across all Stormpath applications. 64 | The username is only unique within a Stormpath application. 65 | """ 66 | try: 67 | accounts = APPLICATION.accounts.search({'email': self.cleaned_data['email']}) 68 | if len(accounts): 69 | msg = 'User with that email already exists.' 70 | raise forms.ValidationError(msg) 71 | except Error as e: 72 | raise forms.ValidationError(str(e)) 73 | 74 | return self.cleaned_data['email'] 75 | 76 | def save(self, commit=True): 77 | user = super(StormpathUserCreationForm, self).save(commit=False) 78 | user.set_password(self.cleaned_data["password1"]) 79 | 80 | if commit: 81 | user.save() 82 | 83 | return user 84 | 85 | 86 | class StormpathUserChangeForm(forms.ModelForm): 87 | """Update Stormpath user form.""" 88 | 89 | class Meta: 90 | model = get_user_model() 91 | exclude = ('password',) 92 | 93 | password = ReadOnlyPasswordHashField(help_text=('Passwords are not stored in the local database but only on Stormpath. You can change the password using this form.')) 94 | 95 | 96 | class PasswordResetEmailForm(forms.Form): 97 | """Form for password reset email.""" 98 | 99 | email = forms.CharField(max_length=255) 100 | 101 | def clean(self): 102 | try: 103 | self.cleaned_data['email'] 104 | return self.cleaned_data 105 | except KeyError: 106 | raise forms.ValidationError('Please provide an email address.') 107 | 108 | def save(self): 109 | APPLICATION.send_password_reset_email(self.cleaned_data['email']) 110 | 111 | 112 | class PasswordResetForm(forms.Form): 113 | """Form for new password input.""" 114 | 115 | new_password1 = forms.CharField(label='New password', widget=forms.PasswordInput) 116 | new_password2 = forms.CharField(label='New password confirmation', widget=forms.PasswordInput) 117 | 118 | def clean_new_password2(self): 119 | """Check if passwords match and are valid.""" 120 | password1 = self.cleaned_data.get('new_password1') 121 | password2 = self.cleaned_data.get('new_password2') 122 | 123 | try: 124 | directory = APPLICATION.default_account_store_mapping.account_store 125 | directory.password_policy.strength.validate_password(password2) 126 | except ValueError as e: 127 | raise forms.ValidationError(str(e)) 128 | 129 | if password1 and password2: 130 | if password1 != password2: 131 | raise forms.ValidationError("The two passwords didn't match.") 132 | 133 | return password2 134 | 135 | def save(self, token): 136 | APPLICATION.reset_account_password(token, self.cleaned_data['new_password1']) 137 | -------------------------------------------------------------------------------- /django_stormpath/helpers.py: -------------------------------------------------------------------------------- 1 | """Library helpers.""" 2 | 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | 7 | def validate_settings(settings): 8 | """Ensure all user-supplied settings exist, or throw a useful error message. 9 | 10 | :param obj settings: The Django settings object. 11 | """ 12 | if not (settings.STORMPATH_ID and settings.STORMPATH_SECRET): 13 | raise ImproperlyConfigured('Both STORMPATH_ID and STORMPATH_SECRET must be specified in settings.py.') 14 | 15 | if not settings.STORMPATH_APPLICATION: 16 | raise ImproperlyConfigured('STORMPATH_APPLICATION must be specified in settings.py.') 17 | -------------------------------------------------------------------------------- /django_stormpath/id_site.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import login as django_login 2 | from django.contrib.auth import logout as django_logout 3 | from django.http import HttpResponseRedirect 4 | from django.shortcuts import resolve_url 5 | from django.conf import settings 6 | 7 | from .backends import StormpathIdSiteBackend 8 | 9 | 10 | ID_SITE_STATUS_AUTHENTICATED = 'AUTHENTICATED' 11 | ID_SITE_STATUS_LOGOUT = 'LOGOUT' 12 | ID_SITE_STATUS_REGISTERED = 'REGISTERED' 13 | 14 | ID_SITE_AUTH_BACKEND = 'django_stormpath.backends.StormpathIdSiteBackend' 15 | 16 | 17 | def _get_django_user(account): 18 | backend = StormpathIdSiteBackend() 19 | return backend.authenticate(account=account) 20 | 21 | 22 | def _handle_authenticated(request, id_site_response): 23 | user = _get_django_user(id_site_response.account) 24 | user.backend = ID_SITE_AUTH_BACKEND 25 | django_login(request, user) 26 | redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) 27 | return HttpResponseRedirect(redirect_to) 28 | 29 | 30 | def _handle_logout(request, id_site_response): 31 | django_logout(request) 32 | redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) 33 | 34 | return HttpResponseRedirect(redirect_to) 35 | 36 | 37 | _handle_registered = _handle_authenticated 38 | 39 | 40 | def handle_id_site_callback(request, id_site_response): 41 | if id_site_response: 42 | action = CALLBACK_ACTIONS[id_site_response.status] 43 | return action(request, id_site_response) 44 | else: 45 | return None 46 | 47 | 48 | CALLBACK_ACTIONS = { 49 | ID_SITE_STATUS_AUTHENTICATED: _handle_authenticated, 50 | ID_SITE_STATUS_LOGOUT: _handle_logout, 51 | ID_SITE_STATUS_REGISTERED: _handle_registered, 52 | } 53 | -------------------------------------------------------------------------------- /django_stormpath/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/django_stormpath/management/__init__.py -------------------------------------------------------------------------------- /django_stormpath/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/django_stormpath/management/commands/__init__.py -------------------------------------------------------------------------------- /django_stormpath/management/commands/create_social_directory.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | try: 6 | input = raw_input 7 | except NameError: 8 | pass 9 | 10 | from stormpath.error import Error as StormpathError 11 | from stormpath.resources.provider import Provider 12 | 13 | from django_stormpath import social 14 | 15 | 16 | class Command(BaseCommand): 17 | help = 'Creates a Directory used for logging in with Google, Facebook, Github or Linkedin.' 18 | 19 | def handle(self, **options): 20 | providers = [Provider.GOOGLE, Provider.FACEBOOK, Provider.GITHUB, Provider.LINKEDIN] 21 | print(""" 22 | 1. Google 23 | 2. Facebook 24 | 3. Github 25 | 4. Linkedin 26 | """) 27 | 28 | p = input('Please choose a provider: ') 29 | try: 30 | provider_choice = int(p) - 1 # off by one 31 | except ValueError: 32 | print('Please choose on of the available provider from the list.') 33 | sys.exit(-1) 34 | 35 | provider = providers[provider_choice] 36 | 37 | redirect_uri = input('Please specify the FQDN redirect uri for this provider: ') 38 | 39 | if not redirect_uri.startswith('http'): 40 | print('Invalid redirect URI. Please enter a FQDN URI like http://example.com/social-login/google/callback') 41 | sys.exit(-1) 42 | 43 | try: 44 | social.create_provider_directory(provider, redirect_uri) 45 | print('Successfully created Directory for {}'.format(provider)) 46 | except StormpathError as e: 47 | print('Error! {}'.format(e)) 48 | sys.exit(-1) 49 | 50 | -------------------------------------------------------------------------------- /django_stormpath/management/commands/sync_accounts_from_stormpath.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import datetime 4 | 5 | from django.core.management.base import BaseCommand 6 | from django_stormpath.models import APPLICATION, StormpathUserManager 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Syncs remote accounts to the local database.' 11 | 12 | def handle(self, **options): 13 | try: 14 | user_manager = StormpathUserManager() 15 | start_time = datetime.datetime.now() 16 | user_manager.sync_accounts_from_stormpath() 17 | duration = datetime.datetime.now() - start_time 18 | print('Successfully synced accounts from {} directory in {}'.format(APPLICATION.name, duration)) 19 | except Exception as e: 20 | print('Error! {}'.format(e)) 21 | sys.exit(-1) 22 | 23 | -------------------------------------------------------------------------------- /django_stormpath/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('auth', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='StormpathUser', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('password', models.CharField(max_length=128, verbose_name='password')), 20 | ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login', null=True, blank=True)), 21 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 22 | ('href', models.CharField(max_length=255, null=True, blank=True)), 23 | ('username', models.CharField(unique=True, max_length=255)), 24 | ('given_name', models.CharField(max_length=255)), 25 | ('surname', models.CharField(max_length=255)), 26 | ('middle_name', models.CharField(max_length=255, null=True, blank=True)), 27 | ('email', models.EmailField(unique=True, max_length=255, verbose_name=b'email address', db_index=True)), 28 | ('is_active', models.BooleanField(default=True)), 29 | ('is_admin', models.BooleanField(default=False)), 30 | ('is_staff', models.BooleanField(default=False)), 31 | ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), 32 | ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | bases=(models.Model,), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /django_stormpath/migrations/0002_auto_20150826_1154.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_stormpath.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_stormpath', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='stormpathuser', 17 | name='is_verified', 18 | field=models.BooleanField(default=False), 19 | ), 20 | migrations.AlterField( 21 | model_name='stormpathuser', 22 | name='groups', 23 | field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'), 24 | ), 25 | migrations.AlterField( 26 | model_name='stormpathuser', 27 | name='is_active', 28 | field=models.BooleanField(default=django_stormpath.models.get_default_is_active), 29 | ), 30 | migrations.AlterField( 31 | model_name='stormpathuser', 32 | name='last_login', 33 | field=models.DateTimeField(null=True, verbose_name='last login', blank=True), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /django_stormpath/migrations/0003_auto_20160426_1425.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_stormpath', '0002_auto_20150826_1154'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='stormpathuser', 16 | name='email', 17 | field=models.EmailField(db_index=True, max_length=255, unique=True, verbose_name='email address'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_stormpath/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/django_stormpath/migrations/__init__.py -------------------------------------------------------------------------------- /django_stormpath/models.py: -------------------------------------------------------------------------------- 1 | """Custom Django User models for Stormpath. 2 | 3 | Any application that uses django_stormpath must provide a user model with a 4 | href field. The href is used in the authentication backend to keep track which 5 | remote Stormpath user the local user represents. It is meant to be used in an 6 | application that modifies user data on Stormpath. If needing to add more 7 | fields please extend the StormpathUser class from this module. 8 | """ 9 | 10 | from django.conf import settings 11 | from django.db import models, IntegrityError, transaction 12 | from django.contrib.auth.models import (BaseUserManager, 13 | AbstractBaseUser, PermissionsMixin) 14 | from django.forms import model_to_dict 15 | from django.core.exceptions import ObjectDoesNotExist 16 | from django.db.models.signals import pre_save, pre_delete 17 | from django.contrib.auth.models import Group 18 | from django.dispatch import receiver 19 | from django import VERSION as django_version 20 | 21 | from stormpath.client import Client 22 | from stormpath.error import Error as StormpathError 23 | from stormpath.resources import AccountCreationPolicy 24 | 25 | from django_stormpath import __version__ 26 | from django_stormpath.helpers import validate_settings 27 | 28 | 29 | # Ensure all user settings have been properly initialized, otherwise we'll 30 | # throw useful error messages to the user so they know what to fix. 31 | validate_settings(settings) 32 | 33 | 34 | # Initialize our Stormpath Client / Application objects -- this way we have 35 | # singletons that can be used throughout our Django sessions. 36 | USER_AGENT = 'stormpath-django/%s django/%s' % (__version__, django_version) 37 | 38 | CLIENT = Client( 39 | id = settings.STORMPATH_ID, 40 | secret = settings.STORMPATH_SECRET, 41 | user_agent = USER_AGENT, 42 | cache_options = getattr(settings, 'STORMPATH_CACHE_OPTIONS', None) 43 | ) 44 | 45 | APPLICATION = CLIENT.applications.get(settings.STORMPATH_APPLICATION) 46 | 47 | 48 | def get_default_is_active(): 49 | """ 50 | Stormpath user is active by default if e-mail verification is 51 | disabled. 52 | """ 53 | directory = APPLICATION.default_account_store_mapping.account_store 54 | verif_email = directory.account_creation_policy.verification_email_status 55 | return verif_email == AccountCreationPolicy.EMAIL_STATUS_DISABLED 56 | 57 | 58 | class StormpathUserManager(BaseUserManager): 59 | 60 | def get(self, *args, **kwargs): 61 | try: 62 | password = kwargs.pop('password') 63 | except KeyError: 64 | password = None 65 | 66 | user = super(StormpathUserManager, self).get(*args, **kwargs) 67 | 68 | if password: 69 | try: 70 | APPLICATION.authenticate_account( 71 | getattr(user, user.USERNAME_FIELD), password) 72 | except StormpathError: 73 | raise self.model.DoesNotExist 74 | 75 | return user 76 | 77 | def create(self, *args, **kwargs): 78 | return self.create_user(*args, **kwargs) 79 | 80 | def get_or_create(self, **kwargs): 81 | try: 82 | return self.get(**kwargs), False 83 | except self.model.DoesNotExist: 84 | return self.create(**kwargs), True 85 | 86 | def update_or_create(self, defaults=None, **kwargs): 87 | defaults = defaults or {} 88 | try: 89 | user = self.get(**kwargs) 90 | except self.model.DoesNotExist: 91 | kwargs.update(defaults) 92 | return self.create(**kwargs), True 93 | 94 | if 'password' in defaults: 95 | user.set_password(defaults.pop('password')) 96 | for k, v in defaults.items(): 97 | setattr(user, k, v) 98 | user.save(using=self._db) 99 | user._remove_raw_password() 100 | return user, False 101 | 102 | def _create_user(self, email, given_name, surname, password): 103 | if not email: 104 | raise ValueError("Users must have an email address") 105 | 106 | if not given_name or not surname: 107 | raise ValueError("Users must provide a given name and a surname") 108 | 109 | user = self.model(email=StormpathUserManager.normalize_email(email), 110 | given_name=given_name, surname=surname) 111 | 112 | user.set_password(password) 113 | user.save(using=self._db) 114 | user._remove_raw_password() 115 | return user 116 | 117 | def create_user(self, email, given_name=None, surname=None, password=None, 118 | first_name=None, last_name=None): 119 | if first_name and not given_name: 120 | given_name = first_name 121 | if last_name and not surname: 122 | surname = last_name 123 | 124 | return self._create_user(email=email, given_name=given_name, surname=surname, 125 | password=password) 126 | 127 | def create_superuser(self, **kwargs): 128 | user = self.create_user(**kwargs) 129 | user.is_admin = True 130 | user.is_staff = True 131 | user.is_superuser = True 132 | user.save(using=self._db) 133 | user._remove_raw_password() 134 | return user 135 | 136 | def delete(self, *args, **kwargs): 137 | for user in self.get_queryset(): 138 | user.delete(*args, **kwargs) 139 | 140 | # Clear the result cache, in case this QuerySet gets reused. 141 | self._result_cache = None 142 | 143 | def sync_accounts_from_stormpath(self, sync_groups=True): 144 | """ :arg sync_groups: WARNING!!! Groups will be deleted from stormpath 145 | if not present locally when user logs in! 146 | 147 | Sync accounts from stormpath -> local database. 148 | This may take a long time, depending on how many users you have in your 149 | Stormpath application. It also makes numerous database queries. 150 | 151 | This method updates local users from stormpath or creates new ones 152 | where the user does not exist locally. This is an additive operation, 153 | meaning it should delete no data from the local database OR stormpath. 154 | """ 155 | if sync_groups: 156 | sp_groups = [g.name for g in APPLICATION.groups] 157 | db_groups = set(Group.objects.all().values_list('name', flat=True)) 158 | missing_from_db = set(sp_groups).difference(db_groups) 159 | if missing_from_db: 160 | groups_to_create = [] 161 | for g_name in missing_from_db: 162 | groups_to_create.append(Group(name=g_name)) 163 | Group.objects.bulk_create(groups_to_create) 164 | 165 | for account in APPLICATION.accounts: 166 | try: 167 | user = StormpathUser.objects.get(email=account.email) 168 | created = True 169 | except StormpathUser.DoesNotExist: 170 | user = StormpathUser() 171 | created = True 172 | user._mirror_data_from_stormpath_account(account) 173 | user.set_unusable_password() 174 | 175 | if created: 176 | user._save_db_only() 177 | 178 | if sync_groups: 179 | users_sp_groups = [g.name for g in account.groups] 180 | user.groups = Group.objects.filter(name__in=users_sp_groups) 181 | user._save_db_only() 182 | 183 | delete.alters_data = True 184 | delete.queryset_only = True 185 | 186 | 187 | class StormpathBaseUser(AbstractBaseUser, PermissionsMixin): 188 | 189 | class Meta: 190 | abstract = True 191 | 192 | href = models.CharField(max_length=255, null=True, blank=True) 193 | username = models.CharField(max_length=255, unique=True) 194 | given_name = models.CharField(max_length=255) 195 | surname = models.CharField(max_length=255) 196 | middle_name = models.CharField(max_length=255, null=True, blank=True) 197 | email = models.EmailField(verbose_name='email address', 198 | max_length=255, 199 | unique=True, 200 | db_index=True) 201 | 202 | STORMPATH_BASE_FIELDS = ['href', 'username', 'given_name', 'surname', 'middle_name', 'email', 'password'] 203 | EXCLUDE_FIELDS = ['href', 'last_login', 'groups', 'id', 'stormpathpermissionsmixin_ptr', 'user_permissions'] 204 | 205 | PASSWORD_FIELD = 'password' 206 | 207 | USERNAME_FIELD = 'email' 208 | REQUIRED_FIELDS = ['given_name', 'surname'] 209 | 210 | is_active = models.BooleanField(default=get_default_is_active) 211 | is_verified = models.BooleanField(default=False) 212 | is_admin = models.BooleanField(default=False) 213 | is_staff = models.BooleanField(default=False) 214 | 215 | objects = StormpathUserManager() 216 | 217 | DJANGO_PREFIX = 'spDjango_' 218 | 219 | @property 220 | def first_name(self): 221 | """This property is added to make Stormpath user compatible 222 | with Django user (first_name is used instead of given_name). 223 | """ 224 | return self.given_name 225 | 226 | @first_name.setter 227 | def first_name(self, value): 228 | self.given_name = value 229 | 230 | @property 231 | def last_name(self): 232 | """This property is added to make Stormpath user compatible 233 | with Django user (last_name is used instead of surname). 234 | """ 235 | return self.surname 236 | 237 | @last_name.setter 238 | def last_name(self, value): 239 | self.surname = value 240 | 241 | def _mirror_data_from_db_user(self, account, data): 242 | for field in self.EXCLUDE_FIELDS: 243 | if field in data: 244 | del data[field] 245 | 246 | if data['is_active']: 247 | account.status = account.STATUS_ENABLED 248 | elif data['is_verified']: 249 | account.status = account.STATUS_DISABLED 250 | else: 251 | account.status = account.STATUS_UNVERIFIED 252 | 253 | if 'is_active' in data: 254 | del data['is_active'] 255 | 256 | for key in data: 257 | if key in self.STORMPATH_BASE_FIELDS: 258 | account[key] = data[key] 259 | else: 260 | account.custom_data[self.DJANGO_PREFIX + key] = data[key] 261 | 262 | return account 263 | 264 | def _mirror_data_from_stormpath_account(self, account): 265 | for field in self.STORMPATH_BASE_FIELDS: 266 | # The password is not sent via the API 267 | # so we take care here to not try and 268 | # mirror it because it's not there 269 | if field != 'password': 270 | self.__setattr__(field, account[field]) 271 | for key in account.custom_data.keys(): 272 | self.__setattr__(key.split(self.DJANGO_PREFIX)[0], account.custom_data[key]) 273 | 274 | if account.status == account.STATUS_ENABLED: 275 | self.is_active = True 276 | self.is_verified = not get_default_is_active() 277 | else: 278 | self.is_active = False 279 | if account.status == account.STATUS_UNVERIFIED: 280 | self.is_verified = False 281 | 282 | def _save_sp_group_memberships(self, account): 283 | try: 284 | db_groups = self.groups.values_list('name', flat=True) 285 | for g in db_groups: 286 | if not account.has_group(g): 287 | account.add_group(g) 288 | 289 | account.save() 290 | 291 | for gm in account.group_memberships: 292 | if gm.group.name not in db_groups: 293 | gm.delete() 294 | except Exception: 295 | raise IntegrityError("Unable to save group memberships.") 296 | 297 | def _create_stormpath_user(self, data, raw_password): 298 | data['password'] = raw_password 299 | account = APPLICATION.accounts.create(data) 300 | self._save_sp_group_memberships(account) 301 | return account 302 | 303 | def _update_stormpath_user(self, data, raw_password): 304 | # if password has changed 305 | if raw_password: 306 | data['password'] = raw_password 307 | else: 308 | # don't set the password if it hasn't changed 309 | del data['password'] 310 | try: 311 | acc = APPLICATION.accounts.get(data.get('href')) 312 | # materialize it 313 | acc.email 314 | 315 | acc = self._mirror_data_from_db_user(acc, data) 316 | acc.save() 317 | self._save_sp_group_memberships(acc) 318 | return acc 319 | except StormpathError as e: 320 | if e.status == 404: 321 | raise self.DoesNotExist('Could not find Stormpath User.') 322 | else: 323 | raise e 324 | finally: 325 | self._remove_raw_password() 326 | 327 | def get_full_name(self): 328 | return "%s %s" % (self.given_name, self.surname) 329 | 330 | def get_short_name(self): 331 | return self.email 332 | 333 | def __unicode__(self): 334 | return self.get_full_name() 335 | 336 | def _update_for_db_and_stormpath(self, *args, **kwargs): 337 | try: 338 | with transaction.atomic(): 339 | super(StormpathBaseUser, self).save(*args, **kwargs) 340 | self._update_stormpath_user(model_to_dict(self), self._get_raw_password()) 341 | except StormpathError: 342 | raise 343 | except ObjectDoesNotExist: 344 | self.delete() 345 | raise 346 | except Exception: 347 | raise 348 | 349 | def _create_for_db_and_stormpath(self, *args, **kwargs): 350 | try: 351 | with transaction.atomic(): 352 | super(StormpathBaseUser, self).save(*args, **kwargs) 353 | account = self._create_stormpath_user(model_to_dict(self), self._get_raw_password()) 354 | self.href = account.href 355 | self.username = account.username 356 | self.save(*args, **kwargs) 357 | except StormpathError: 358 | raise 359 | except Exception: 360 | # we're not sure if we have a href yet, hence we 361 | # filter by email 362 | accounts = APPLICATION.accounts.search({'email': self.email}) 363 | if accounts: 364 | accounts[0].delete() 365 | raise 366 | 367 | def _save_db_only(self, *args, **kwargs): 368 | super(StormpathBaseUser, self).save(*args, **kwargs) 369 | 370 | def _remove_raw_password(self): 371 | """We need to send a raw password to Stormpath. After an Account is saved on Stormpath 372 | we need to remove the raw password field from the local object""" 373 | 374 | try: 375 | del self.raw_password 376 | except AttributeError: 377 | pass 378 | 379 | def _get_raw_password(self): 380 | try: 381 | return self.raw_password 382 | except AttributeError: 383 | return None 384 | 385 | def set_password(self, raw_password): 386 | """We don't want to keep passwords locally""" 387 | self.set_unusable_password() 388 | self.raw_password = raw_password 389 | 390 | def check_password(self, raw_password): 391 | try: 392 | acc = APPLICATION.authenticate_account(self.username, raw_password) 393 | return acc is not None 394 | except StormpathError as e: 395 | # explicity check to see if password is incorrect 396 | if e.code == 7100: 397 | return False 398 | raise e 399 | 400 | def save(self, *args, **kwargs): 401 | self.username = getattr(self, self.USERNAME_FIELD) 402 | # Are we updating an existing User? 403 | if self.id: 404 | self._update_for_db_and_stormpath(*args, **kwargs) 405 | # Or are we creating a new user? 406 | else: 407 | self._create_for_db_and_stormpath(*args, **kwargs) 408 | 409 | def delete(self, *args, **kwargs): 410 | with transaction.atomic(): 411 | href = self.href 412 | super(StormpathBaseUser, self).delete(*args, **kwargs) 413 | try: 414 | account = APPLICATION.accounts.get(href) 415 | account.delete() 416 | except StormpathError: 417 | raise 418 | 419 | 420 | class StormpathUser(StormpathBaseUser): 421 | pass 422 | 423 | 424 | @receiver(pre_save, sender=Group) 425 | def save_group_to_stormpath(sender, instance, **kwargs): 426 | try: 427 | if instance.pk is None: 428 | # creating a new group 429 | APPLICATION.groups.create({'name': instance.name}) 430 | else: 431 | # updating an existing group 432 | old_group = Group.objects.get(pk=instance.pk) 433 | remote_groups = APPLICATION.groups.search({'name': old_group.name}) 434 | if len(remote_groups) is 0: 435 | # group existed locally but not on Stormpath, create it 436 | APPLICATION.groups.create({'name': instance.name}) 437 | return 438 | 439 | remote_group = remote_groups[0] 440 | 441 | if remote_group.name == instance.name: 442 | return # nothing changed 443 | 444 | remote_group.name = instance.name 445 | remote_group.save() 446 | 447 | except StormpathError as e: 448 | raise IntegrityError(e) 449 | 450 | 451 | @receiver(pre_delete, sender=Group) 452 | def delete_group_from_stormpath(sender, instance, **kwargs): 453 | try: 454 | APPLICATION.groups.search({'name': instance.name})[0].delete() 455 | except StormpathError as e: 456 | raise IntegrityError(e) 457 | -------------------------------------------------------------------------------- /django_stormpath/social.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import login as django_login 2 | from django.shortcuts import resolve_url 3 | from django.core.urlresolvers import reverse 4 | from django.conf import settings 5 | 6 | 7 | from stormpath.error import Error as StormpathError 8 | from stormpath.resources.provider import Provider 9 | from requests_oauthlib import OAuth2Session 10 | 11 | from .models import CLIENT, APPLICATION 12 | from .backends import StormpathSocialBackend 13 | 14 | SOCIAL_AUTH_BACKEND = 'django_stormpath.backends.StormpathSocialBackend' 15 | 16 | GITHUB_AUTHORIZATION_BASE_URL = 'https://github.com/login/oauth/authorize' 17 | GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token' 18 | 19 | GOOGLE_AUTHORIZATION_BASE_URL = 'https://accounts.google.com/o/oauth2/auth' 20 | GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' 21 | 22 | FACEBOOK_AUTHORIZATION_BASE_URL = 'https://www.facebook.com/dialog/oauth' 23 | FACEBOOK_TOKEN_URL = 'https://graph.facebook.com/oauth/access_token' 24 | 25 | LINKEDIN_AUTHORIZATION_BASE_URL = 'https://www.linkedin.com/uas/oauth2/authorization' 26 | LINKEDIN_TOKEN_URL = 'https://www.linkedin.com/uas/oauth2/accessToken' 27 | 28 | 29 | def _get_django_user(account): 30 | backend = StormpathSocialBackend() 31 | return backend.authenticate(account=account) 32 | 33 | 34 | def get_access_token(provider, authorization_response, redirect_uri): 35 | if provider == Provider.GOOGLE: 36 | p = OAuth2Session( 37 | client_id=settings.STORMPATH_SOCIAL['GOOGLE']['client_id'], 38 | redirect_uri=redirect_uri 39 | ) 40 | ret = p.fetch_token( 41 | GOOGLE_TOKEN_URL, 42 | client_secret=settings.STORMPATH_SOCIAL['GOOGLE']['client_secret'], 43 | authorization_response=authorization_response 44 | ) 45 | 46 | return ret['access_token'] 47 | elif provider == Provider.FACEBOOK: 48 | p = OAuth2Session( 49 | client_id=settings.STORMPATH_SOCIAL['FACEBOOK']['client_id'], 50 | redirect_uri=redirect_uri 51 | ) 52 | 53 | from requests_oauthlib.compliance_fixes import facebook_compliance_fix 54 | 55 | p = facebook_compliance_fix(p) 56 | ret = p.fetch_token( 57 | FACEBOOK_TOKEN_URL, 58 | client_secret=settings.STORMPATH_SOCIAL['FACEBOOK']['client_secret'], 59 | authorization_response=authorization_response 60 | ) 61 | 62 | return ret['access_token'] 63 | elif provider == Provider.GITHUB or provider.upper() == Provider.GITHUB: 64 | p = OAuth2Session(client_id=settings.STORMPATH_SOCIAL['GITHUB']['client_id']) 65 | ret = p.fetch_token( 66 | GITHUB_TOKEN_URL, 67 | client_secret=settings.STORMPATH_SOCIAL['GITHUB']['client_secret'], 68 | authorization_response=authorization_response 69 | ) 70 | 71 | return ret['access_token'] 72 | elif provider == Provider.LINKEDIN: 73 | p = OAuth2Session( 74 | client_id=settings.STORMPATH_SOCIAL['LINKEDIN']['client_id'], 75 | redirect_uri=redirect_uri 76 | ) 77 | 78 | from requests_oauthlib.compliance_fixes import linkedin_compliance_fix 79 | 80 | p = linkedin_compliance_fix(p) 81 | ret = p.fetch_token( 82 | LINKEDIN_TOKEN_URL, 83 | client_secret=settings.STORMPATH_SOCIAL['LINKEDIN']['client_secret'], 84 | authorization_response=authorization_response 85 | ) 86 | 87 | return ret['access_token'] 88 | else: 89 | return None 90 | 91 | 92 | def handle_social_callback(request, provider): 93 | provider_redirect_url = 'stormpath_' + provider.lower() + '_login_callback' 94 | abs_redirect_uri = request.build_absolute_uri(reverse(provider_redirect_url, kwargs={'provider': provider})) 95 | access_token = get_access_token(provider, request.build_absolute_uri(), abs_redirect_uri) 96 | 97 | if not access_token: 98 | raise RuntimeError('Error communicating with Autentication Provider: {}'.format(provider)) 99 | 100 | params = {'provider': provider, 'access_token': access_token} 101 | 102 | try: 103 | account = APPLICATION.get_provider_account(**params) 104 | except StormpathError as e: 105 | # We might be missing a social directory 106 | # First we look for one and see if it's already there 107 | # and just error out 108 | for asm in APPLICATION.account_store_mappings: 109 | if (getattr(asm.account_store, 'provider') and asm.account_store.provider.provider_id == provider): 110 | raise e 111 | 112 | # Or if we couldn't find one we create it for the user 113 | # map it to the current application 114 | # and try authenticate again 115 | create_provider_directory(provider, abs_redirect_uri) 116 | account = APPLICATION.get_provider_account(**params) 117 | 118 | user = _get_django_user(account) 119 | user.backend = SOCIAL_AUTH_BACKEND 120 | django_login(request, user) 121 | redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) 122 | 123 | return redirect_to 124 | 125 | 126 | def create_provider_directory(provider, redirect_uri): 127 | """Helper function for creating a provider directory""" 128 | dir = CLIENT.directories.create({ 129 | 'name': APPLICATION.name + '-' + provider, 130 | 'provider': { 131 | 'client_id': settings.STORMPATH_SOCIAL[provider.upper()]['client_id'], 132 | 'client_secret': settings.STORMPATH_SOCIAL[provider.upper()]['client_secret'], 133 | 'redirect_uri': redirect_uri, 134 | 'provider_id': provider, 135 | }, 136 | }) 137 | 138 | APPLICATION.account_store_mappings.create({ 139 | 'application': APPLICATION, 140 | 'account_store': dir, 141 | 'list_index': 99, 142 | 'is_default_account_store': False, 143 | 'is_default_group_store': False, 144 | }) 145 | 146 | 147 | def get_authorization_url(provider, redirect_uri): 148 | if provider == Provider.GOOGLE: 149 | scope = ['email', "profile"] 150 | p = OAuth2Session( 151 | client_id=settings.STORMPATH_SOCIAL['GOOGLE']['client_id'], 152 | scope=scope, 153 | redirect_uri=redirect_uri 154 | ) 155 | authorization_url, state = p.authorization_url(GOOGLE_AUTHORIZATION_BASE_URL) 156 | 157 | return authorization_url, state 158 | 159 | elif provider == Provider.FACEBOOK: 160 | p = OAuth2Session( 161 | client_id=settings.STORMPATH_SOCIAL['FACEBOOK']['client_id'], 162 | redirect_uri=redirect_uri 163 | ) 164 | 165 | from requests_oauthlib.compliance_fixes import facebook_compliance_fix 166 | 167 | p = facebook_compliance_fix(p) 168 | authorization_url, state = p.authorization_url(FACEBOOK_AUTHORIZATION_BASE_URL) 169 | 170 | return authorization_url, state 171 | 172 | elif provider == Provider.GITHUB or provider.upper() == Provider.GITHUB: 173 | p = OAuth2Session(client_id=settings.STORMPATH_SOCIAL['GITHUB']['client_id']) 174 | authorization_url, state = p.authorization_url(GITHUB_AUTHORIZATION_BASE_URL) 175 | 176 | return authorization_url, state 177 | 178 | elif provider == Provider.LINKEDIN: 179 | p = OAuth2Session( 180 | client_id=settings.STORMPATH_SOCIAL['LINKEDIN']['client_id'], 181 | redirect_uri=redirect_uri 182 | ) 183 | 184 | from requests_oauthlib.compliance_fixes import linkedin_compliance_fix 185 | 186 | p = linkedin_compliance_fix(p) 187 | authorization_url, state = p.authorization_url(LINKEDIN_AUTHORIZATION_BASE_URL) 188 | 189 | return authorization_url, state 190 | else: 191 | raise RuntimeError('Invalid Provider {}'.format(provider)) 192 | -------------------------------------------------------------------------------- /django_stormpath/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'StormpathUser' 12 | db.create_table(u'django_stormpath_stormpathuser', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('password', self.gf('django.db.models.fields.CharField')(max_length=128)), 15 | ('last_login', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), 16 | ('is_superuser', self.gf('django.db.models.fields.BooleanField')(default=False)), 17 | ('href', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), 18 | ('username', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), 19 | ('given_name', self.gf('django.db.models.fields.CharField')(max_length=255)), 20 | ('surname', self.gf('django.db.models.fields.CharField')(max_length=255)), 21 | ('middle_name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), 22 | ('email', self.gf('django.db.models.fields.EmailField')(unique=True, max_length=255, db_index=True)), 23 | ('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)), 24 | ('is_admin', self.gf('django.db.models.fields.BooleanField')(default=False)), 25 | ('is_staff', self.gf('django.db.models.fields.BooleanField')(default=False)), 26 | )) 27 | db.send_create_signal(u'django_stormpath', ['StormpathUser']) 28 | 29 | # Adding M2M table for field groups on 'StormpathUser' 30 | m2m_table_name = db.shorten_name(u'django_stormpath_stormpathuser_groups') 31 | db.create_table(m2m_table_name, ( 32 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 33 | ('stormpathuser', models.ForeignKey(orm[u'django_stormpath.stormpathuser'], null=False)), 34 | ('group', models.ForeignKey(orm[u'auth.group'], null=False)) 35 | )) 36 | db.create_unique(m2m_table_name, ['stormpathuser_id', 'group_id']) 37 | 38 | # Adding M2M table for field user_permissions on 'StormpathUser' 39 | m2m_table_name = db.shorten_name(u'django_stormpath_stormpathuser_user_permissions') 40 | db.create_table(m2m_table_name, ( 41 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 42 | ('stormpathuser', models.ForeignKey(orm[u'django_stormpath.stormpathuser'], null=False)), 43 | ('permission', models.ForeignKey(orm[u'auth.permission'], null=False)) 44 | )) 45 | db.create_unique(m2m_table_name, ['stormpathuser_id', 'permission_id']) 46 | 47 | 48 | def backwards(self, orm): 49 | # Deleting model 'StormpathUser' 50 | db.delete_table(u'django_stormpath_stormpathuser') 51 | 52 | # Removing M2M table for field groups on 'StormpathUser' 53 | db.delete_table(db.shorten_name(u'django_stormpath_stormpathuser_groups')) 54 | 55 | # Removing M2M table for field user_permissions on 'StormpathUser' 56 | db.delete_table(db.shorten_name(u'django_stormpath_stormpathuser_user_permissions')) 57 | 58 | 59 | models = { 60 | u'auth.group': { 61 | 'Meta': {'object_name': 'Group'}, 62 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 64 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 65 | }, 66 | u'auth.permission': { 67 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 68 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 69 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 70 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 72 | }, 73 | u'contenttypes.contenttype': { 74 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 75 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 76 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 78 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 79 | }, 80 | u'django_stormpath.stormpathuser': { 81 | 'Meta': {'object_name': 'StormpathUser'}, 82 | 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), 83 | 'given_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 84 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), 85 | 'href': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 86 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 87 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 88 | 'is_admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 89 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 90 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 91 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 92 | 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 93 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 94 | 'surname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 95 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), 96 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) 97 | } 98 | } 99 | 100 | complete_apps = ['django_stormpath'] -------------------------------------------------------------------------------- /django_stormpath/south_migrations/0002_auto__add_field_stormpathuser_is_verified.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'StormpathUser.is_verified' 12 | db.add_column(u'django_stormpath_stormpathuser', 'is_verified', 13 | self.gf('django.db.models.fields.BooleanField')(default=False), 14 | keep_default=False) 15 | 16 | 17 | def backwards(self, orm): 18 | # Deleting field 'StormpathUser.is_verified' 19 | db.delete_column(u'django_stormpath_stormpathuser', 'is_verified') 20 | 21 | 22 | models = { 23 | u'auth.group': { 24 | 'Meta': {'object_name': 'Group'}, 25 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 27 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 28 | }, 29 | u'auth.permission': { 30 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 31 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 32 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 33 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 35 | }, 36 | u'contenttypes.contenttype': { 37 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 38 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 39 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 40 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 41 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 42 | }, 43 | u'django_stormpath.stormpathuser': { 44 | 'Meta': {'object_name': 'StormpathUser'}, 45 | 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), 46 | 'given_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 47 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), 48 | 'href': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 49 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 51 | 'is_admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 52 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 53 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 54 | 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 55 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 56 | 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 57 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 58 | 'surname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 59 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), 60 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) 61 | } 62 | } 63 | 64 | complete_apps = ['django_stormpath'] -------------------------------------------------------------------------------- /django_stormpath/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/django_stormpath/south_migrations/__init__.py -------------------------------------------------------------------------------- /django_stormpath/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.conf import settings 3 | 4 | from django_stormpath import views 5 | 6 | 7 | urlpatterns = [ 8 | url(r'^login/$', views.stormpath_id_site_login, name='stormpath_id_site_login'), 9 | url(r'^logout/$', views.stormpath_id_site_logout, name='stormpath_id_site_logout'), 10 | url(r'^register/$', views.stormpath_id_site_register, name='stormpath_id_site_register'), 11 | url(r'^forgot-password/$', views.stormpath_id_site_forgot_password, name='stormpath_id_site_forgot_password'), 12 | url(r'^handle-callback/(?Pstormpath)', views.stormpath_callback, name='stormpath_id_site_callback'), 13 | ] 14 | 15 | if getattr(settings, 'STORMPATH_ENABLE_GOOGLE', False): 16 | urlpatterns += [ 17 | url(r'handle-callback/(?Pgoogle)', views.stormpath_callback, 18 | name='stormpath_google_login_callback'), 19 | url(r'^social-login/(?Pgoogle)/', views.stormpath_social_login, 20 | name='stormpath_google_social_login'), 21 | ] 22 | if getattr(settings, 'STORMPATH_ENABLE_FACEBOOK', False): 23 | urlpatterns += [ 24 | url(r'handle-callback/(?Pfacebook)', views.stormpath_callback, 25 | name='stormpath_facebook_login_callback'), 26 | url(r'^social-login/(?Pfacebook)/', views.stormpath_social_login, 27 | name='stormpath_facebook_social_login'), 28 | ] 29 | if getattr(settings, 'STORMPATH_ENABLE_GITHUB', False): 30 | urlpatterns += [ 31 | url(r'handle-callback/(?Pgithub)', views.stormpath_callback, 32 | name='stormpath_github_login_callback'), 33 | url(r'^social-login/(?Pgithub)/', views.stormpath_social_login, 34 | name='stormpath_github_social_login'), 35 | ] 36 | if getattr(settings, 'STORMPATH_ENABLE_LINKEDIN', False): 37 | urlpatterns += [ 38 | url(r'handle-callback/(?Plinkedin)', views.stormpath_callback, 39 | name='stormpath_linkedin_login_callback'), 40 | url(r'^social-login/(?Plinkedin)/', views.stormpath_social_login, 41 | name='stormpath_linkedin_social_login'), 42 | ] 43 | 44 | if django.VERSION[:2] < (1, 8): 45 | from django.conf.urls import patterns 46 | urlpatterns = patterns('django_stormpath.views', *urlpatterns) 47 | 48 | -------------------------------------------------------------------------------- /django_stormpath/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import redirect 3 | from django.http import HttpResponseBadRequest 4 | from django.core.urlresolvers import reverse 5 | 6 | from stormpath.resources.provider import Provider 7 | 8 | from .models import APPLICATION 9 | from .id_site import handle_id_site_callback 10 | from .social import get_authorization_url, handle_social_callback 11 | 12 | 13 | def stormpath_callback(request, provider): 14 | if provider == 'stormpath': 15 | ret = APPLICATION.handle_stormpath_callback( 16 | request.build_absolute_uri()) 17 | return handle_id_site_callback(request, ret) 18 | 19 | rdr = handle_social_callback(request, provider) 20 | return redirect(rdr) 21 | 22 | 23 | def stormpath_id_site_login(request): 24 | rdr = APPLICATION.build_id_site_redirect_url( 25 | callback_uri=settings.STORMPATH_ID_SITE_CALLBACK_URI, 26 | state=request.GET.get('state')) 27 | return redirect(rdr) 28 | 29 | 30 | def stormpath_id_site_register(request): 31 | rdr = APPLICATION.build_id_site_redirect_url( 32 | callback_uri=settings.STORMPATH_ID_SITE_CALLBACK_URI, 33 | state=request.GET.get('state'), 34 | path="/#/register") 35 | return redirect(rdr) 36 | 37 | 38 | def stormpath_id_site_forgot_password(request): 39 | rdr = APPLICATION.build_id_site_redirect_url( 40 | callback_uri=settings.STORMPATH_ID_SITE_CALLBACK_URI, 41 | state=request.GET.get('state'), 42 | path="/#/forgot") 43 | return redirect(rdr) 44 | 45 | 46 | def stormpath_id_site_logout(request): 47 | rdr = APPLICATION.build_id_site_redirect_url( 48 | callback_uri=settings.STORMPATH_ID_SITE_CALLBACK_URI, 49 | state=request.GET.get('state'), 50 | logout=True) 51 | return redirect(rdr) 52 | 53 | 54 | def stormpath_social_login(request, provider): 55 | redirect_uri = request.build_absolute_uri( 56 | reverse('stormpath_' + provider + '_login_callback', kwargs={'provider': provider})) 57 | authorization_url, sate = get_authorization_url(provider, redirect_uri) 58 | return redirect(authorization_url) 59 | 60 | 61 | def stormpath_social_login_callback(request, provider): 62 | rdr = handle_social_callback(request, provider) 63 | return redirect(rdr) 64 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-stormpath.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-stormpath.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-stormpath" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-stormpath" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/assets/images/stormpath-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/docs/assets/images/stormpath-logo.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-stormpath documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Aug 26 14:38:12 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import django 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | sys.path.insert(0, os.path.abspath('../testproject')) 24 | from django_stormpath import __version__, __short_version__ 25 | 26 | from testproject.utils import set_env 27 | 28 | 29 | set_env('../testproject/.env') 30 | 31 | django.setup() 32 | 33 | # -- General configuration ----------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | #needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be extensions 39 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 40 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig'] 41 | 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix of source filenames. 47 | source_suffix = '.rst' 48 | 49 | # The encoding of source files. 50 | #source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'django-stormpath' 57 | copyright = '2014, Stormpath Inc.' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = __short_version__ 65 | # The full version, including alpha/beta/rc tags. 66 | release = __version__ 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | #language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = ['_build'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | 106 | # -- Options for HTML output --------------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | html_theme = 'default' 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | #html_theme_options = {} 116 | 117 | # Add any paths that contain custom themes here, relative to this directory. 118 | #html_theme_path = [] 119 | 120 | # The name for this set of Sphinx documents. If None, it defaults to 121 | # " v documentation". 122 | #html_title = None 123 | 124 | # A shorter title for the navigation bar. Default is the same as html_title. 125 | #html_short_title = None 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | html_logo = 'assets/images/stormpath-logo.png' 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | #html_favicon = None 135 | 136 | # Add any paths that contain custom static files (such as style sheets) here, 137 | # relative to this directory. They are copied after the builtin static files, 138 | # so a file named "default.css" will overwrite the builtin "default.css". 139 | html_static_path = ['_static'] 140 | 141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 142 | # using the given strftime format. 143 | #html_last_updated_fmt = '%b %d, %Y' 144 | 145 | # If true, SmartyPants will be used to convert quotes and dashes to 146 | # typographically correct entities. 147 | #html_use_smartypants = True 148 | 149 | # Custom sidebar templates, maps document names to template names. 150 | #html_sidebars = {} 151 | 152 | # Additional templates that should be rendered to pages, maps page names to 153 | # template names. 154 | #html_additional_pages = {} 155 | 156 | # If false, no module index is generated. 157 | #html_domain_indices = True 158 | 159 | # If false, no index is generated. 160 | #html_use_index = True 161 | 162 | # If true, the index is split into individual pages for each letter. 163 | #html_split_index = False 164 | 165 | # If true, links to the reST sources are added to the pages. 166 | #html_show_sourcelink = True 167 | 168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 169 | #html_show_sphinx = True 170 | 171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 172 | #html_show_copyright = True 173 | 174 | # If true, an OpenSearch description file will be output, and all pages will 175 | # contain a tag referring to it. The value of this option must be the 176 | # base URL from which the finished HTML is served. 177 | #html_use_opensearch = '' 178 | 179 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 180 | #html_file_suffix = None 181 | 182 | # Output file base name for HTML help builder. 183 | htmlhelp_basename = 'django-stormpathdoc' 184 | 185 | 186 | # -- Options for LaTeX output -------------------------------------------------- 187 | 188 | latex_elements = { 189 | # The paper size ('letterpaper' or 'a4paper'). 190 | #'papersize': 'letterpaper', 191 | 192 | # The font size ('10pt', '11pt' or '12pt'). 193 | #'pointsize': '10pt', 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #'preamble': '', 197 | } 198 | 199 | # Grouping the document tree into LaTeX files. List of tuples 200 | # (source start file, target name, title, author, documentclass [howto/manual]). 201 | latex_documents = [ 202 | ('index', 'django-stormpath.tex', 'django-stormpath Documentation', 203 | 'Goran Cetusic', 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | #latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | #latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output -------------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ('index', 'django-stormpath', 'django-stormpath Documentation', 233 | ['Goran Cetusic'], 1) 234 | ] 235 | 236 | # If true, show URL addresses after external links. 237 | #man_show_urls = False 238 | 239 | 240 | # -- Options for Texinfo output ------------------------------------------------ 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'django-stormpath', 'django-stormpath Documentation', 247 | 'Goran Cetusic', 'django-stormpath', 'One line description of project.', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | #texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | #texinfo_no_detailmenu = False 262 | -------------------------------------------------------------------------------- /docs/django_stormpath.rst: -------------------------------------------------------------------------------- 1 | django_stormpath Package 2 | ======================== 3 | 4 | :mod:`backends` Module 5 | ---------------------- 6 | 7 | .. automodule:: django_stormpath.backends 8 | :members: 9 | :show-inheritance: 10 | 11 | :mod:`models` Module 12 | -------------------- 13 | 14 | .. automodule:: django_stormpath.models 15 | :members: 16 | :show-inheritance: 17 | 18 | :mod:`forms` Module 19 | -------------------- 20 | 21 | .. automodule:: django_stormpath.forms 22 | :members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-stormpath documentation master file, created by 2 | sphinx-quickstart on Mon Aug 26 14:38:12 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-stormpath's documentation! 7 | ============================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | django_stormpath 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`search` 21 | 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-stormpath.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-stormpath.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.2.3 2 | pydispatch==1.1.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import chdir, environ, system 2 | from subprocess import call 3 | from sys import exit 4 | 5 | from setuptools import setup, find_packages, Command 6 | 7 | from django_stormpath import __version__ 8 | 9 | 10 | class BaseCommand(Command): 11 | user_options = [] 12 | 13 | def initialize_options(self): 14 | pass 15 | 16 | def finalize_options(self): 17 | pass 18 | 19 | 20 | class TestCommand(BaseCommand): 21 | 22 | description = 'run self-tests' 23 | 24 | def run(self): 25 | chdir('testproject') 26 | ret = system('coverage run --source=django_stormpath manage.py test --settings=testproject.settings testapp && coverage html') 27 | 28 | if ret != 0: 29 | exit(-1) 30 | else: 31 | exit(0) 32 | 33 | 34 | class DocCommand(BaseCommand): 35 | 36 | description = 'generate documentation' 37 | 38 | def run(self): 39 | environ['DJANGO_SETTINGS_MODULE'] = 'testproject.settings' 40 | try: 41 | chdir('docs') 42 | ret = system('make html') 43 | exit(ret) 44 | except OSError as e: 45 | print(e) 46 | exit(-1) 47 | 48 | 49 | setup( 50 | name = 'django-stormpath', 51 | version = __version__, 52 | author = 'Stormpath, Inc.', 53 | author_email = 'python@stormpath.com', 54 | description = 'Stormpath integration for Django.', 55 | license = 'Apache', 56 | url = 'https://github.com/stormpath/stormpath-django', 57 | zip_safe = False, 58 | classifiers = [ 59 | 'Development Status :: 4 - Beta', 60 | 'Environment :: Web Environment', 61 | 'Framework :: Django', 62 | 'Intended Audience :: Developers', 63 | 'Programming Language :: Python', 64 | 'Topic :: Software Development :: Libraries :: Python Modules', 65 | ], 66 | packages = find_packages(), 67 | install_requires = [ 68 | 'requests-oauthlib>=0.4.2', 69 | 'stormpath>=2.1.8', 70 | 'Django>=1.6', 71 | ], 72 | extras_require = { 73 | 'test': ['codacy-coverage', 'python-coveralls', 'coverage'], 74 | }, 75 | cmdclass = { 76 | 'test': TestCommand, 77 | 'docs': DocCommand, 78 | }, 79 | ) 80 | -------------------------------------------------------------------------------- /testproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | from testproject.utils import set_env 6 | 7 | sys.path.insert(0, os.path.abspath( 8 | os.path.join(os.path.dirname(__file__), ".."))) 9 | 10 | if __name__ == "__main__": 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") 12 | set_env('.env') 13 | from django.core.management import execute_from_command_line 14 | 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /testproject/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/testproject/testapp/__init__.py -------------------------------------------------------------------------------- /testproject/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/index.html: -------------------------------------------------------------------------------- 1 | {% if user.is_authenticated %} 2 |

Logged in as: {{ user.email }}

3 |

Logout

4 | {% else %} 5 |

Not logged in.

6 |

Login

7 |

Register

8 |

Forgot password

9 | {% endif %} 10 | -------------------------------------------------------------------------------- /testproject/testapp/tests.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from uuid import uuid4 3 | 4 | from django.test import TestCase 5 | from django.db import IntegrityError, transaction 6 | from django.contrib.auth import get_user_model 7 | from django.core.exceptions import ObjectDoesNotExist 8 | from django.contrib.auth.models import Group 9 | 10 | import django_stormpath 11 | from django_stormpath.models import CLIENT 12 | from django_stormpath.backends import StormpathBackend 13 | from django_stormpath.forms import * 14 | 15 | from pydispatch import dispatcher 16 | 17 | from stormpath.error import Error as StormpathError 18 | from stormpath.resources.base import SIGNAL_RESOURCE_CREATED 19 | 20 | 21 | def sleep_receiver_function(signal, sender, data, params): 22 | """Sleeps for one second. 23 | 24 | This is used to sleep for a second after every Stormpath resource is 25 | created, to avoid test failures. 26 | """ 27 | sleep(1) 28 | 29 | 30 | # Connect the signal. 31 | dispatcher.connect(sleep_receiver_function, signal=SIGNAL_RESOURCE_CREATED) 32 | 33 | 34 | UserModel = get_user_model() 35 | 36 | 37 | class LiveTestBase(TestCase): 38 | 39 | def setUp(self): 40 | super(LiveTestBase, self).setUp() 41 | 42 | self.prefix = 'stormpath-django-test-%s' % uuid4().hex 43 | self.app = CLIENT.applications.create({'name': self.prefix}, create_directory = True) 44 | django_stormpath.models.APPLICATION = self.app 45 | 46 | def tearDown(self): 47 | super(LiveTestBase, self).tearDown() 48 | 49 | for mapping in self.app.account_store_mappings: 50 | mapping.account_store.delete() 51 | 52 | self.app.delete() 53 | 54 | def create_django_user(self, superuser=False, email=None, password=None, 55 | custom_data=None, given_name=None, surname=None, first_name=None, 56 | last_name=None): 57 | rnd = uuid4().hex 58 | 59 | if email is None: 60 | email = rnd + '@example.com' 61 | if given_name is None and first_name is None: 62 | given_name = 'Given ' + rnd 63 | if surname is None and last_name is None: 64 | surname = 'Sur ' + rnd 65 | if password is None: 66 | password = 'W00t123!' + rnd 67 | 68 | props = { 69 | 'email': email, 70 | 'given_name': given_name, 71 | 'surname': surname, 72 | 'password': password, 73 | 'first_name': first_name, 74 | 'last_name': last_name, 75 | } 76 | 77 | if superuser: 78 | user = UserModel.objects.create_superuser(**props) 79 | else: 80 | user = UserModel.objects.create_user(**props) 81 | 82 | return user 83 | 84 | 85 | class TestUserAndGroups(LiveTestBase): 86 | def test_creating_a_user(self): 87 | user = self.create_django_user( 88 | email='john.doe1@example.com', 89 | given_name='John', 90 | surname='Doe', 91 | password='TestPassword123!', 92 | ) 93 | 94 | a = self.app.accounts.get(user.href) 95 | self.assertEqual(user.href, a.href) 96 | 97 | def test_creating_a_superuser(self): 98 | user = self.create_django_user( 99 | superuser=True, 100 | email='john.doe2@example.com', 101 | given_name='John', 102 | surname='Doe', 103 | password='TestPassword123!', 104 | ) 105 | 106 | a = self.app.accounts.get(user.href) 107 | self.assertEqual(user.href, a.href) 108 | self.assertEqual(a.custom_data['spDjango_is_staff'], True) 109 | self.assertEqual(a.custom_data['spDjango_is_superuser'], True) 110 | 111 | def test_updating_a_user(self): 112 | user = self.create_django_user( 113 | email='john.doe3@example.com', 114 | given_name='John', 115 | surname='Doe', 116 | password='TestPassword123!', 117 | ) 118 | 119 | a = self.app.accounts.get(user.href) 120 | self.assertEqual(user.href, a.href) 121 | 122 | user.surname = 'Smith' 123 | user.save() 124 | 125 | a = self.app.accounts.get(user.href) 126 | self.assertEqual(user.surname, a.surname) 127 | 128 | def test_get_with_non_existing(self): 129 | UserModel.objects.create_user( 130 | email='me1@example.com', 131 | given_name='Sample', 132 | surname='User', 133 | password='TestPassword123!', 134 | ) 135 | 136 | with self.assertRaises(UserModel.DoesNotExist): 137 | UserModel.objects.get(email='does.not@exist.com') 138 | 139 | def test_get_with_existing(self): 140 | UserModel.objects.create_user( 141 | email='me2@example.com', 142 | given_name='Sample', 143 | surname='User', 144 | password='TestPassword123!', 145 | ) 146 | 147 | user = UserModel.objects.get( 148 | email='me2@example.com', 149 | given_name='Sample', 150 | surname='User', 151 | password='TestPassword123!', 152 | ) 153 | 154 | self.assertTrue(user) 155 | 156 | def test_get_wrong_password(self): 157 | UserModel.objects.create_user( 158 | email='me3@example.com', 159 | given_name='Sample', 160 | surname='User', 161 | password='TestPassword123!', 162 | ) 163 | 164 | with self.assertRaises(UserModel.DoesNotExist): 165 | UserModel.objects.get( 166 | email='me3@example.com', 167 | given_name='Sample', 168 | surname='User', 169 | password='wrong', 170 | ) 171 | 172 | def test_get_or_create_user_with_non_existing(self): 173 | user, created = UserModel.objects.get_or_create( 174 | email='me4@example.com', 175 | given_name='Sample', 176 | surname='User', 177 | password='TestPassword123!', 178 | ) 179 | 180 | self.assertTrue(created) 181 | self.assertEqual(self.app.accounts.get(user.href).given_name, 'Sample') 182 | user = UserModel.objects.get(email='me4@example.com') 183 | self.assertEqual(user.surname, 'User') 184 | self.assertIsNotNone(StormpathBackend().authenticate('me4@example.com', 'TestPassword123!')) 185 | 186 | def test_get_or_create_user_with_wrong_password(self): 187 | UserModel.objects.create_user( 188 | email='me5@example.com', 189 | given_name='Sample', 190 | surname='User', 191 | password='TestPassword123!', 192 | ) 193 | 194 | with self.assertRaises(IntegrityError): 195 | user, created = UserModel.objects.get_or_create( 196 | email='me5@example.com', 197 | given_name='Sample', 198 | surname='User', 199 | password='wrong', 200 | ) 201 | 202 | def test_get_or_create_user_with_existing(self): 203 | UserModel.objects.create_user( 204 | email='me6@example.com', 205 | given_name='Sample', 206 | surname='User', 207 | password='TestPassword123!', 208 | ) 209 | 210 | user, created = UserModel.objects.get_or_create( 211 | email='me6@example.com', 212 | given_name='Sample', 213 | surname='User', 214 | password='TestPassword123!', 215 | ) 216 | 217 | self.assertFalse(created) 218 | self.assertEqual(self.app.accounts.get(user.href).given_name, 'Sample') 219 | user = UserModel.objects.get(email='me6@example.com') 220 | self.assertEqual(user.surname, 'User') 221 | self.assertIsNotNone(StormpathBackend().authenticate('me6@example.com', 'TestPassword123!')) 222 | 223 | def test_update_or_create_user_with_non_existing(self): 224 | user, created = UserModel.objects.update_or_create( 225 | defaults={'given_name': 'Updated'}, 226 | email='me7@example.com', 227 | given_name='Sample', 228 | surname='User', 229 | password='TestPassword123!', 230 | ) 231 | 232 | self.assertTrue(created) 233 | self.assertEqual(self.app.accounts.get(user.href).given_name, 'Updated') 234 | user = UserModel.objects.get(email='me7@example.com') 235 | self.assertEqual(user.given_name, 'Updated') 236 | self.assertEqual(user.surname, 'User') 237 | self.assertIsNotNone(StormpathBackend().authenticate('me7@example.com', 'TestPassword123!')) 238 | 239 | def test_update_or_create_user_with_wrong_password(self): 240 | UserModel.objects.create_user( 241 | email='me8@example.com', 242 | given_name='Sample', 243 | surname='User', 244 | password='TestPassword123!', 245 | ) 246 | 247 | with self.assertRaises(IntegrityError): 248 | user, created = UserModel.objects.update_or_create( 249 | defaults={'given_name': 'Updated'}, 250 | email='me8@example.com', 251 | given_name='Sample', 252 | surname='User', 253 | password='123!TestPassword', 254 | ) 255 | 256 | def test_update_or_create_user_with_non_existing_and_password(self): 257 | user, created = UserModel.objects.update_or_create( 258 | defaults={'given_name': 'Updated', 'password': '123!TestPassword'}, 259 | email='me9@example.com', 260 | given_name='Sample', 261 | surname='User', 262 | password='TestPassword123!', 263 | ) 264 | 265 | self.assertTrue(created) 266 | self.assertEqual(self.app.accounts.get(user.href).given_name, 'Updated') 267 | user = UserModel.objects.get(email='me9@example.com') 268 | self.assertEqual(user.given_name, 'Updated') 269 | self.assertEqual(user.surname, 'User') 270 | self.assertIsNotNone(StormpathBackend().authenticate('me9@example.com', '123!TestPassword')) 271 | 272 | def test_update_or_create_user_with_existing(self): 273 | UserModel.objects.create_user( 274 | email='me10@example.com', 275 | given_name='Sample', 276 | surname='User', 277 | password='TestPassword123!', 278 | ) 279 | 280 | user, created = UserModel.objects.update_or_create( 281 | defaults={'given_name': 'Updated'}, 282 | email='me10@example.com', 283 | given_name='Sample', 284 | surname='User', 285 | password='TestPassword123!', 286 | ) 287 | 288 | self.assertFalse(created) 289 | self.assertEqual(self.app.accounts.get(user.href).given_name, 'Updated') 290 | user = UserModel.objects.get(email='me10@example.com') 291 | self.assertEqual(user.given_name, 'Updated') 292 | self.assertEqual(user.surname, 'User') 293 | self.assertIsNotNone(StormpathBackend().authenticate('me10@example.com', 'TestPassword123!')) 294 | 295 | def test_update_or_create_user_with_existing_and_password(self): 296 | UserModel.objects.create_user( 297 | email='me11@example.com', 298 | given_name='Sample', 299 | surname='User', 300 | password='TestPassword123!', 301 | ) 302 | 303 | user, created = UserModel.objects.update_or_create( 304 | defaults={'given_name': 'Updated', 'password': '123!TestPassword'}, 305 | email='me11@example.com', 306 | given_name='Sample', 307 | surname='User', 308 | ) 309 | 310 | self.assertFalse(created) 311 | self.assertEqual(self.app.accounts.get(user.href).given_name, 'Updated') 312 | user = UserModel.objects.get(email='me11@example.com') 313 | self.assertEqual(user.given_name, 'Updated') 314 | self.assertEqual(user.surname, 'User') 315 | self.assertIsNotNone(StormpathBackend().authenticate('me11@example.com', '123!TestPassword')) 316 | 317 | def test_updating_a_user_with_invalid_fields_should_not_delete_user(self): 318 | """ 319 | Issue https://github.com/stormpath/stormpath-django/issues/49 320 | """ 321 | user = self.create_django_user( 322 | email='john.doe3@example.com', 323 | given_name='John', 324 | surname='Doe', 325 | password='TestPassword123!', 326 | ) 327 | 328 | a = self.app.accounts.get(user.href) 329 | user.email = '' 330 | 331 | with self.assertRaises(StormpathError): 332 | user.save() 333 | 334 | a = self.app.accounts.get(user.href) 335 | self.assertEqual(a.email, 'john.doe3@example.com') 336 | 337 | def test_updating_nonexistent_user_deletes_that_user(self): 338 | user = self.create_django_user( 339 | email='john.doe3@example.com', 340 | given_name='John', 341 | surname='Doe', 342 | password='TestPassword123!', 343 | ) 344 | 345 | a = self.app.accounts.get(user.href) 346 | self.assertEqual(user.href, a.href) 347 | 348 | a.delete() 349 | 350 | user = UserModel.objects.get(email='john.doe3@example.com') 351 | user.given_name = 'Johnny' 352 | 353 | with self.assertRaises(user.DoesNotExist): 354 | user.save() 355 | 356 | with self.assertRaises(UserModel.DoesNotExist): 357 | UserModel.objects.get(email='john.doe3@example.com') 358 | 359 | def test_authentication_pulls_user_into_local_db(self): 360 | self.assertEqual(0, UserModel.objects.count()) 361 | acc = self.app.accounts.create({ 362 | 'email': 'jd@example.com', 363 | 'given_name': 'John', 364 | 'surname': 'Doe', 365 | 'password': 'TestPassword123!', 366 | }) 367 | 368 | b = StormpathBackend() 369 | b.authenticate(acc.email, 'TestPassword123!') 370 | self.assertEqual(1, UserModel.objects.count()) 371 | 372 | def test_authentication_updates_user_info_in_local_db(self): 373 | self.assertEqual(0, UserModel.objects.count()) 374 | acc = self.app.accounts.create({ 375 | 'email': 'jd@example.com', 376 | 'given_name': 'John', 377 | 'surname': 'Doe', 378 | 'password': 'TestPassword123!', 379 | }) 380 | 381 | b = StormpathBackend() 382 | b.authenticate(acc.email, 'TestPassword123!') 383 | self.assertEqual(1, UserModel.objects.count()) 384 | 385 | acc.surname = 'Test' 386 | acc.save() 387 | 388 | user = b.authenticate(acc.email, 'TestPassword123!') 389 | self.assertEqual(1, UserModel.objects.count()) 390 | self.assertEqual('Test', user.surname) 391 | 392 | def test_auth_doesnt_work_for_bogus_user(self): 393 | b = StormpathBackend() 394 | 395 | u = b.authenticate('nonexistent@example.com', 'TestPassword123!') 396 | self.assertIsNone(u) 397 | self.assertEqual(0, UserModel.objects.count()) 398 | 399 | def test_groups_get_pulled_in_on_authentication(self): 400 | self.assertEqual(0, UserModel.objects.count()) 401 | self.assertEqual(0, Group.objects.count()) 402 | acc = self.app.accounts.create({ 403 | 'email': 'jd2@example.com', 404 | 'given_name': 'John', 405 | 'surname': 'Doe', 406 | 'password': 'TestPassword123!!!', 407 | }) 408 | 409 | g1 = self.app.groups.create({'name': 'testGroup'}) 410 | g2 = self.app.groups.create({'name': 'testGroup2'}) # noqa 411 | 412 | acc.add_group(g1) 413 | acc.save() 414 | 415 | b = StormpathBackend() 416 | user = b.authenticate(acc.email, 'TestPassword123!!!') 417 | 418 | self.assertEqual(1, UserModel.objects.count()) 419 | self.assertEqual(2, Group.objects.count()) 420 | self.assertEqual(1, user.groups.filter(name=g1.name).count()) 421 | 422 | def test_groups_get_created_on_stormpath(self): 423 | self.assertEqual(0, Group.objects.count()) 424 | Group.objects.create(name='testGroup') 425 | 426 | self.assertEqual(1, Group.objects.count()) 427 | self.assertEqual(1, len(self.app.groups)) 428 | 429 | def test_group_creation_errors_are_handled(self): 430 | self.assertEqual(0, Group.objects.count()) 431 | Group.objects.create(name='testGroup') 432 | 433 | self.assertEqual(1, Group.objects.count()) 434 | self.assertEqual(1, len(self.app.groups)) 435 | 436 | self.app.groups.create({'name': 'exists'}) 437 | 438 | self.assertRaises(IntegrityError, Group.objects.create, **{'name': 'exists'}) 439 | self.assertEqual(1, Group.objects.count()) 440 | self.assertEqual(2, len(self.app.groups)) 441 | 442 | def test_group_creation_error_on_local_db(self): 443 | self.assertEqual(0, Group.objects.count()) 444 | Group.objects.create(name='testGroup') 445 | 446 | self.assertEqual(1, Group.objects.count()) 447 | self.assertEqual(1, len(self.app.groups)) 448 | 449 | # we delete the image from stormpath 450 | self.app.groups.search({'name': 'testGroup'})[0].delete() 451 | 452 | # and try to re-create a duplicate locally 453 | with transaction.atomic(): 454 | # we need to do manual commits here because django won't let us 455 | # do more queries until we commit 456 | self.assertRaises(IntegrityError, 457 | Group.objects.create, **{'name': 'testGroup'}) 458 | 459 | self.assertEqual(1, Group.objects.count()) 460 | self.assertEqual(1, len(self.app.groups)) 461 | 462 | def test_deleting_a_user(self): 463 | self.assertEqual(0, UserModel.objects.count()) 464 | user = self.create_django_user( 465 | email='john.doe1@example.com', 466 | given_name='John', 467 | surname='Doe', 468 | password='TestPassword123!', 469 | ) 470 | 471 | a = self.app.accounts.get(user.href) 472 | self.assertEqual(user.href, a.href) 473 | self.assertEqual(1, UserModel.objects.count()) 474 | 475 | href = user.href 476 | user.delete() 477 | 478 | self.assertEqual(0, UserModel.objects.count()) 479 | a = self.app.accounts.get(href) 480 | self.assertRaises(StormpathError, a.__getattr__, 'email') 481 | 482 | def test_deleting_users(self): 483 | self.assertEqual(0, UserModel.objects.count()) 484 | user_1 = self.create_django_user( 485 | email='john.doe1@example.com', 486 | given_name='John2', 487 | surname='Doe', 488 | password='TestPassword123!', 489 | ) 490 | 491 | user_2 = self.create_django_user( 492 | email='john.doe2@example.com', 493 | given_name='John', 494 | surname='Doe', 495 | password='TestPassword123!', 496 | ) 497 | 498 | href_1, href_2 = user_1.href, user_2.href 499 | self.assertEqual(set([a.href for a in self.app.accounts]), {href_1, href_2}) 500 | self.assertEqual(2, UserModel.objects.count()) 501 | UserModel.objects.delete() 502 | 503 | self.assertEqual(0, UserModel.objects.count()) 504 | a = self.app.accounts.get(href_1) 505 | self.assertRaises(StormpathError, a.__getattr__, 'email') 506 | a = self.app.accounts.get(href_2) 507 | self.assertRaises(StormpathError, a.__getattr__, 'email') 508 | 509 | def test_deleteing_a_group(self): 510 | self.assertEqual(0, Group.objects.count()) 511 | Group.objects.create(name='testGroup') 512 | 513 | self.assertEqual(1, Group.objects.count()) 514 | self.assertEqual(1, len(self.app.groups)) 515 | 516 | Group.objects.all().delete() 517 | 518 | self.assertEqual(0, Group.objects.count()) 519 | self.assertEqual(0, len(self.app.groups)) 520 | 521 | def test_saving_group_membership(self): 522 | self.assertEqual(0, UserModel.objects.count()) 523 | user = self.create_django_user( 524 | email='john.doe1@example.com', 525 | given_name='John', 526 | surname='Doe', 527 | password='TestPassword123!', 528 | ) 529 | 530 | g = Group.objects.create(name='testGroup') 531 | user.groups.add(g) 532 | user.save() 533 | 534 | a = self.app.accounts.get(href=user.href) 535 | 536 | self.assertEqual(1, UserModel.objects.count()) 537 | self.assertEqual(1, Group.objects.count()) 538 | self.assertEqual(1, user.groups.filter(name=g.name).count()) 539 | self.assertEqual(1, len(a.group_memberships)) 540 | 541 | user.groups.all().delete() 542 | 543 | g = self.app.groups.create({'name': 'testGroup'}) 544 | CLIENT.group_memberships.create({'account': a, 'group': g}) 545 | 546 | user.save() 547 | self.assertEqual(1, len(self.app.groups)) 548 | self.assertEqual(0, len(a.group_memberships)) 549 | 550 | def test_updating_non_existent_sp_user(self): 551 | self.assertEqual(0, UserModel.objects.count()) 552 | user = self.create_django_user( 553 | email='john.doe1@example.com', 554 | given_name='John', 555 | surname='Doe', 556 | password='TestPassword123!', 557 | ) 558 | 559 | a = self.app.accounts.get(href=user.href) 560 | a.delete() 561 | 562 | self.assertRaises(ObjectDoesNotExist, user.save) 563 | 564 | def test_updating_a_user_that_doesnt_exists_on_sp(self): 565 | self.assertEqual(0, UserModel.objects.count()) 566 | user = self.create_django_user( 567 | email='john.doe1@example.com', 568 | given_name='John', 569 | surname='Doe', 570 | password='TestPassword123!', 571 | ) 572 | 573 | self.assertEqual(1, UserModel.objects.count()) 574 | user.delete() 575 | 576 | self.assertEqual(0, UserModel.objects.count()) 577 | self.assertEqual(0, len(self.app.accounts)) 578 | 579 | def test_creating_a_user_with_invalid_password(self): 580 | self.assertEqual(0, UserModel.objects.count()) 581 | self.assertRaises(StormpathError, self.create_django_user, 582 | email='john.doe1@example.com', 583 | given_name='John', 584 | surname='Doe', 585 | password='invalidpassword', 586 | ) # stormpath pass policy 587 | 588 | self.assertEqual(0, UserModel.objects.count()) 589 | self.assertEqual(0, len(self.app.accounts)) 590 | 591 | def test_valid_check_password(self): 592 | self.assertEqual(0, UserModel.objects.count()) 593 | 594 | user = self.create_django_user( 595 | email='john.doe1@example.com', 596 | given_name='John', 597 | surname='Doe', 598 | password='TestPassword123!', 599 | ) 600 | 601 | self.assertTrue(user.check_password('TestPassword123!')) 602 | 603 | def test_invalid_check_password(self): 604 | self.assertEqual(0, UserModel.objects.count()) 605 | 606 | user = self.create_django_user( 607 | email='john.doe1@example.com', 608 | given_name='John', 609 | surname='Doe', 610 | password='TestPassword123!', 611 | ) 612 | 613 | self.assertFalse(user.check_password('invalidpassword')) 614 | 615 | 616 | class TestDjangoUser(LiveTestBase): 617 | def test_creating_a_user(self): 618 | user = self.create_django_user( 619 | email='john.doe1@example.com', 620 | first_name='John', 621 | last_name='Doe', 622 | password='TestPassword123!', 623 | ) 624 | 625 | a = self.app.accounts.get(user.href) 626 | self.assertEqual(user.href, a.href) 627 | self.assertEqual(user.first_name, user.given_name) 628 | self.assertEqual(user.first_name, a.given_name) 629 | self.assertEqual(user.last_name, user.surname) 630 | self.assertEqual(user.last_name, a.surname) 631 | 632 | def test_creating_a_superuser(self): 633 | user = self.create_django_user( 634 | superuser=True, 635 | email='john.doe2@example.com', 636 | first_name='John', 637 | last_name='Doe', 638 | password='TestPassword123!', 639 | ) 640 | 641 | a = self.app.accounts.get(user.href) 642 | self.assertEqual(user.href, a.href) 643 | self.assertEqual(user.first_name, user.given_name) 644 | self.assertEqual(user.first_name, a.given_name) 645 | self.assertEqual(user.last_name, user.surname) 646 | self.assertEqual(user.last_name, a.surname) 647 | self.assertEqual(a.custom_data['spDjango_is_staff'], True) 648 | self.assertEqual(a.custom_data['spDjango_is_superuser'], True) 649 | 650 | def test_updating_a_user(self): 651 | user = self.create_django_user( 652 | email='john.doe3@example.com', 653 | first_name='John', 654 | last_name='Doe', 655 | password='TestPassword123!', 656 | ) 657 | 658 | a = self.app.accounts.get(user.href) 659 | self.assertEqual(user.href, a.href) 660 | 661 | user.surname = 'Smith' 662 | user.save() 663 | 664 | a = self.app.accounts.get(user.href) 665 | self.assertEqual(user.surname, a.surname) 666 | self.assertEqual(user.last_name, user.surname) 667 | self.assertEqual(user.last_name, a.surname) 668 | 669 | user.first_name = 'Jane' 670 | user.save() 671 | 672 | a = self.app.accounts.get(user.href) 673 | self.assertEqual(user.given_name, a.given_name) 674 | self.assertEqual(user.first_name, a.given_name) 675 | self.assertEqual(user.first_name, a.given_name) 676 | 677 | def test_updating_a_users_password(self): 678 | user = self.create_django_user( 679 | email='john.doe3@example.com', 680 | first_name='John', 681 | last_name='Doe', 682 | password='TestPassword123!', 683 | ) 684 | 685 | a = self.app.accounts.get(user.href) 686 | self.assertEqual(user.href, a.href) 687 | 688 | user.set_password('123!TestPassword') 689 | self.assertTrue(hasattr(user, 'raw_password')) 690 | 691 | user.save() 692 | 693 | b = StormpathBackend() 694 | 695 | self.assertFalse(hasattr(user, 'raw_password')) 696 | self.assertFalse(user.has_usable_password()) 697 | self.assertTrue(b.authenticate(a.email, '123!TestPassword')) 698 | self.assertFalse(b.authenticate(a.email, 'TestPassword123!')) 699 | 700 | def test_authentication_pulls_user_into_local_db(self): 701 | self.assertEqual(0, UserModel.objects.count()) 702 | acc = self.app.accounts.create({ 703 | 'email': 'jd@example.com', 704 | 'given_name': 'John', 705 | 'surname': 'Doe', 706 | 'password': 'TestPassword123!', 707 | }) 708 | 709 | b = StormpathBackend() 710 | 711 | b.authenticate(acc.email, 'TestPassword123!') 712 | self.assertEqual(1, UserModel.objects.count()) 713 | 714 | user = UserModel.objects.get() 715 | self.assertEqual(user.first_name, user.given_name) 716 | self.assertEqual(user.first_name, acc.given_name) 717 | self.assertEqual(user.last_name, user.surname) 718 | self.assertEqual(user.last_name, acc.surname) 719 | 720 | def test_user_email_verification_enabled(self): 721 | directory = self.app.default_account_store_mapping.account_store 722 | directory.account_creation_policy.verification_email_status = 'ENABLED' 723 | directory.account_creation_policy.save() 724 | user = self.create_django_user( 725 | email='john.doe3@example.com', 726 | first_name='John', 727 | last_name='Doe', 728 | password='TestPassword123!', 729 | ) 730 | 731 | a = self.app.accounts.get(user.href) 732 | 733 | self.assertFalse(user.is_active) 734 | self.assertFalse(user.is_verified) 735 | self.assertEqual(a.status, a.STATUS_UNVERIFIED) 736 | 737 | a.status = a.STATUS_ENABLED 738 | a.save() 739 | sb = StormpathBackend() 740 | user = sb._create_or_get_user(a) 741 | 742 | self.assertTrue(user.is_active) 743 | self.assertTrue(user.is_verified) 744 | self.assertEqual(a.status, a.STATUS_ENABLED) 745 | 746 | a.status = a.STATUS_DISABLED 747 | a.save() 748 | user = sb._create_or_get_user(a) 749 | self.assertFalse(user.is_active) 750 | self.assertTrue(user.is_verified) 751 | self.assertEqual(a.status, a.STATUS_DISABLED) 752 | 753 | def test_user_email_verification_disabled(self): 754 | user = self.create_django_user( 755 | email='john.doe3@example.com', 756 | first_name='John', 757 | last_name='Doe', 758 | password='TestPassword123!', 759 | ) 760 | 761 | a = self.app.accounts.get(user.href) 762 | 763 | self.assertTrue(user.is_active) 764 | self.assertFalse(user.is_verified) 765 | self.assertEqual(a.status, a.STATUS_ENABLED) 766 | 767 | a.status = a.STATUS_DISABLED 768 | a.save() 769 | sb = StormpathBackend() 770 | user = sb._create_or_get_user(a) 771 | 772 | self.assertFalse(user.is_active) 773 | self.assertFalse(user.is_verified) 774 | self.assertEqual(a.status, a.STATUS_DISABLED) 775 | 776 | a.status = a.STATUS_UNVERIFIED 777 | a.save() 778 | user = sb._create_or_get_user(a) 779 | self.assertFalse(user.is_active) 780 | self.assertFalse(user.is_verified) 781 | self.assertEqual(a.status, a.STATUS_UNVERIFIED) 782 | 783 | 784 | class TestForms(LiveTestBase): 785 | def test_user_creation_form_password_missmatch(self): 786 | data = { 787 | 'email': 'john.doe@example.com', 788 | 'username': 'johndoe', 789 | 'given_name': 'John', 790 | 'surname': 'Doe', 791 | 'password1': 'TestPassword123!', 792 | 'password2': 'TestPassword12345!', 793 | } 794 | 795 | form = StormpathUserCreationForm(data) 796 | is_valid = form.is_valid() 797 | self.assertFalse(is_valid) 798 | self.assertRaises(ValueError, form.save) 799 | 800 | def test_user_creation_form_password_invalid(self): 801 | data = { 802 | 'email': 'john.doe@example.com', 803 | 'username': 'johndoe', 804 | 'given_name': 'John', 805 | 'surname': 'Doe', 806 | 'password1': 'invalid', 807 | 'password2': 'invalid', 808 | } 809 | 810 | form = StormpathUserCreationForm(data) 811 | is_valid = form.is_valid() 812 | self.assertFalse(is_valid) 813 | self.assertRaises(ValueError, form.save) 814 | 815 | def test_user_creation_form_existing_email(self): 816 | self.create_django_user( 817 | email='john.doe@example.com', 818 | given_name='John', 819 | surname='Doe', 820 | password='TestPassword123!', 821 | ) 822 | 823 | data = { 824 | 'email': 'john.doe@example.com', 825 | 'username': 'johndoe', 826 | 'given_name': 'John', 827 | 'surname': 'Doe', 828 | 'password1': 'TestPassword123!', 829 | 'password2': 'TestPassword123!', 830 | } 831 | 832 | form = StormpathUserCreationForm(data) 833 | is_valid = form.is_valid() 834 | self.assertFalse(is_valid) 835 | self.assertRaises(ValueError, form.save) 836 | 837 | def test_user_creation_form_existing_username(self): 838 | user = self.create_django_user( 839 | email='john.doe@example.com', 840 | given_name='John', 841 | surname='Doe', 842 | password='TestPassword123!', 843 | ) 844 | 845 | acc = self.app.accounts.get(href=user.href) 846 | 847 | data = { 848 | 'email': 'john.doe@example.com', 849 | 'given_name': 'John', 850 | 'username': acc.username, 851 | 'surname': 'Doe', 852 | 'password1': 'TestPassword123!', 853 | 'password2': 'TestPassword123!', 854 | } 855 | 856 | form = StormpathUserCreationForm(data) 857 | is_valid = form.is_valid() 858 | 859 | self.assertFalse(is_valid) 860 | self.assertRaises(ValueError, form.save) 861 | 862 | def test_saving_user_form(self): 863 | data = { 864 | 'email': 'john.doe123@example.com', 865 | 'username': 'johndoe', 866 | 'given_name': 'John', 867 | 'surname': 'Doe', 868 | 'password1': 'TestPassword123!', 869 | 'password2': 'TestPassword123!', 870 | } 871 | 872 | form = StormpathUserCreationForm(data) 873 | is_valid = form.is_valid() 874 | 875 | self.assertTrue(is_valid) 876 | form.save() 877 | self.assertEqual(1, UserModel.objects.count()) 878 | -------------------------------------------------------------------------------- /testproject/testapp/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def home(request): 5 | return render(request, 'testapp/index.html', {}) 6 | 7 | -------------------------------------------------------------------------------- /testproject/testproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-django/af60eb5da2115d94ac313613c5d4e6b9f3d16157/testproject/testproject/__init__.py -------------------------------------------------------------------------------- /testproject/testproject/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for django_stormpath project. 2 | 3 | import os 4 | import sys 5 | 6 | from uuid import uuid4 7 | 8 | from stormpath.client import Client 9 | 10 | 11 | ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '.')) 12 | sys.path.insert(0, os.path.abspath(os.path.join(ROOT_DIR, '..'))) 13 | 14 | DEBUG = True 15 | TEMPLATE_DEBUG = DEBUG 16 | 17 | ADMINS = ( 18 | # ('Your Name', 'your_email@example.com'), 19 | ) 20 | 21 | MANAGERS = ADMINS 22 | 23 | DATABASES = { 24 | 'default': { 25 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 26 | 'NAME': 'dev.db', 27 | } 28 | } 29 | 30 | # Hosts/domain names that are valid for this site; required if DEBUG is False 31 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 32 | ALLOWED_HOSTS = [] 33 | 34 | # Local time zone for this installation. Choices can be found here: 35 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 36 | # although not all choices may be available on all operating systems. 37 | # In a Windows environment this must be set to your system time zone. 38 | TIME_ZONE = 'America/Chicago' 39 | 40 | # Language code for this installation. All choices can be found here: 41 | # http://www.i18nguy.com/unicode/language-identifiers.html 42 | LANGUAGE_CODE = 'en-us' 43 | 44 | SITE_ID = 1 45 | 46 | # If you set this to False, Django will make some optimizations so as not 47 | # to load the internationalization machinery. 48 | USE_I18N = True 49 | 50 | # If you set this to False, Django will not format dates, numbers and 51 | # calendars according to the current locale. 52 | USE_L10N = True 53 | 54 | # If you set this to False, Django will not use timezone-aware datetimes. 55 | USE_TZ = True 56 | 57 | # Absolute filesystem path to the directory that will hold user-uploaded files. 58 | # Example: "/var/www/example.com/media/" 59 | MEDIA_ROOT = '' 60 | 61 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 62 | # trailing slash. 63 | # Examples: "http://example.com/media/", "http://media.example.com/" 64 | MEDIA_URL = '' 65 | 66 | # Absolute path to the directory static files should be collected to. 67 | # Don't put anything in this directory yourself; store your static files 68 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 69 | # Example: "/var/www/example.com/static/" 70 | STATIC_ROOT = '' 71 | 72 | # URL prefix for static files. 73 | # Example: "http://example.com/static/", "http://static.example.com/" 74 | STATIC_URL = '/static/' 75 | 76 | # Additional locations of static files 77 | STATICFILES_DIRS = ( 78 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 79 | # Always use forward slashes, even on Windows. 80 | # Don't forget to use absolute paths, not relative paths. 81 | ) 82 | 83 | # List of finder classes that know how to find static files in 84 | # various locations. 85 | STATICFILES_FINDERS = ( 86 | 'django.contrib.staticfiles.finders.FileSystemFinder', 87 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 88 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 89 | ) 90 | 91 | # Make this unique, and don't share it with anybody. 92 | SECRET_KEY = 'l6-03tuo+fhgdhh9+vz%_ip$lfv+ic52k@xu7sgydna*k)y976' 93 | 94 | # List of callables that know how to import templates from various sources. 95 | TEMPLATE_LOADERS = ( 96 | 'django.template.loaders.filesystem.Loader', 97 | 'django.template.loaders.app_directories.Loader', 98 | # 'django.template.loaders.eggs.Loader', 99 | ) 100 | 101 | MIDDLEWARE_CLASSES = ( 102 | 'django.middleware.common.CommonMiddleware', 103 | 'django.contrib.sessions.middleware.SessionMiddleware', 104 | 'django.middleware.csrf.CsrfViewMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | # Uncomment the next line for simple clickjacking protection: 108 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 109 | ) 110 | 111 | ROOT_URLCONF = 'testproject.urls' 112 | 113 | # Python dotted path to the WSGI application used by Django's runserver. 114 | WSGI_APPLICATION = 'testproject.wsgi.application' 115 | 116 | TEMPLATE_DIRS = ( 117 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 118 | # Always use forward slashes, even on Windows. 119 | # Don't forget to use absolute paths, not relative paths. 120 | ) 121 | 122 | INSTALLED_APPS = ( 123 | 'django.contrib.auth', 124 | 'django.contrib.contenttypes', 125 | 'django.contrib.sessions', 126 | 'django.contrib.sites', 127 | 'django.contrib.messages', 128 | 'django.contrib.staticfiles', 129 | 'django.contrib.admin', 130 | 'django_stormpath', 131 | 'testapp' 132 | ) 133 | 134 | AUTHENTICATION_BACKENDS = ( 135 | 'django_stormpath.backends.StormpathBackend', 136 | 'django_stormpath.backends.StormpathIdSiteBackend', 137 | 'django_stormpath.backends.StormpathSocialBackend' 138 | ) 139 | 140 | # A sample logging configuration. The only tangible logging 141 | # performed by this configuration is to send an email to 142 | # the site admins on every HTTP 500 error when DEBUG=False. 143 | # See http://docs.djangoproject.com/en/dev/topics/logging for 144 | # more details on how to customize your logging configuration. 145 | LOGGING = { 146 | 'version': 1, 147 | 'disable_existing_loggers': False, 148 | 'filters': { 149 | 'require_debug_false': { 150 | '()': 'django.utils.log.RequireDebugFalse' 151 | } 152 | }, 153 | 'handlers': { 154 | 'mail_admins': { 155 | 'level': 'ERROR', 156 | 'filters': ['require_debug_false'], 157 | 'class': 'django.utils.log.AdminEmailHandler' 158 | } 159 | }, 160 | 'loggers': { 161 | 'django.request': { 162 | 'handlers': ['mail_admins'], 163 | 'level': 'ERROR', 164 | 'propagate': True, 165 | }, 166 | } 167 | } 168 | 169 | STORMPATH_ID = os.environ['STORMPATH_API_KEY_ID'] 170 | STORMPATH_SECRET = os.environ['STORMPATH_API_KEY_SECRET'] 171 | 172 | # Retrieve our Stormpath built-in application. This won't be used for any 173 | # testing, but is required for the integration to function. 174 | client = Client(id=STORMPATH_ID, secret=STORMPATH_SECRET) 175 | 176 | application = client.applications.create({ 177 | 'name': 'django-test-{}'.format(uuid4().hex), 178 | }, create_directory=True) 179 | 180 | STORMPATH_APPLICATION = application.href 181 | STORMPATH_ID_SITE_CALLBACK_URI = 'http://localhost:8000/stormpath-id-site-callback' 182 | 183 | AUTH_USER_MODEL = 'django_stormpath.StormpathUser' 184 | 185 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 186 | 187 | LOGIN_REDIRECT_URL = '/' 188 | 189 | STORMPATH_ENABLE_GOOGLE = True 190 | STORMPATH_ENABLE_FACEBOOK = True 191 | STORMPATH_ENABLE_GITHUB = True 192 | STORMPATH_ENABLE_LINKEDIN = True 193 | 194 | STORMPATH_SOCIAL = { 195 | 'GOOGLE': { 196 | 'client_id': os.environ['GOOGLE_CLIENT_ID'], 197 | 'client_secret': os.environ['GOOGLE_CLIENT_SECRET'], 198 | }, 199 | 'FACEBOOK': { 200 | 'client_id': os.environ['FACEBOOK_CLIENT_ID'], 201 | 'client_secret': os.environ['FACEBOOK_CLIENT_SECRET'] 202 | }, 203 | 'GITHUB': { 204 | 'client_id': os.environ['GITHUB_CLIENT_ID'], 205 | 'client_secret': os.environ['GITHUB_CLIENT_SECRET'] 206 | }, 207 | 'LINKEDIN': { 208 | 'client_id': os.environ['LINKEDIN_CLIENT_ID'], 209 | 'client_secret': os.environ['LINKEDIN_CLIENT_SECRET'] 210 | }, 211 | } 212 | 213 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 214 | SESSION_COOKIE_SECURE = True 215 | CSRF_COOKIE_SECURE = True 216 | -------------------------------------------------------------------------------- /testproject/testproject/test_settings.py: -------------------------------------------------------------------------------- 1 | 2 | from settings import * 3 | 4 | DATABASES = { 5 | 'default': { 6 | 'ENGINE': 'django.db.backends.sqlite3', 7 | 'NAME': ':memory:', 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testproject/testproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | from django.contrib import admin 5 | import django_stormpath.urls 6 | admin.autodiscover() 7 | 8 | 9 | urlpatterns = patterns('', 10 | 11 | url(r'^$', 'testapp.views.home'), 12 | url(r'', include(django_stormpath.urls)), 13 | url(r'^admin/', include(admin.site.urls)), 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /testproject/testproject/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def set_env(fpath): 5 | try: 6 | with open(fpath) as f: 7 | lines = f.readlines() 8 | for line in lines: 9 | if not line.isspace() and not line.startswith("#"): 10 | k, v = line.split('=') 11 | os.environ[k] = v.strip() 12 | except IOError: 13 | pass 14 | 15 | 16 | -------------------------------------------------------------------------------- /testproject/testproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_stormpath project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "django_stormpath.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | --------------------------------------------------------------------------------