pax_global_header00006660000000000000000000000064131155521150014511gustar00rootroot0000000000000052 comment=bcb94b9827da0755f1784d6ab8f475283bb27a05 django-oauth-toolkit-1.0.0/000077500000000000000000000000001311555211500155525ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/.gitignore000066400000000000000000000006351311555211500175460ustar00rootroot00000000000000*.py[cod] *.swp # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .cache .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # PyCharm stuff .idea # Sphinx build dir _build # Sqlite database files *.sqlite django-oauth-toolkit-1.0.0/.travis.yml000066400000000000000000000014771311555211500176740ustar00rootroot00000000000000language: python python: - "3.5" sudo: false env: - TOXENV=py27-django110 - TOXENV=py27-django111 - TOXENV=py34-django110 - TOXENV=py34-django111 - TOXENV=py35-django110 - TOXENV=py35-django111 - TOXENV=py35-djangomaster - TOXENV=py36-django111 - TOXENV=py36-djangomaster - TOXENV=docs matrix: fast_finish: true include: - python: "3.6" env: TOXENV=py36-django111 - python: "3.6" env: TOXENV=py36-djangomaster exclude: - python: "3.5" env: TOXENV=py36-django111 - python: "3.5" env: TOXENV=py36-djangomaster allow_failures: - env: TOXENV=py35-djangomaster - env: TOXENV=py36-djangomaster cache: directories: - $HOME/.cache/pip - $TRAVIS_BUILD_DIR/.tox install: - pip install coveralls tox script: - tox after_script: - coveralls django-oauth-toolkit-1.0.0/AUTHORS000066400000000000000000000005561311555211500166300ustar00rootroot00000000000000Authors ======= Massimiliano Pippi Federico Frenguelli Contributors ============ Alessandro De Angelis Ash Christopher Aristóbulo Meneses Bart Merenda Bas van Oostveen David Fischer Diego Garcia Emanuele Palazzetti Federico Dolce Hiroki Kiyohara Jens Timmerman Jerome Leclanche Jim Graham Paul Oswald pySilver Rodney Richardson Silvano Cerza Stéphane Raimbault django-oauth-toolkit-1.0.0/CHANGELOG.md000066400000000000000000000175351311555211500173760ustar00rootroot00000000000000### 1.0.0 [2017-06-07] * **New feature**: AccessToken, RefreshToken and Grant models are now swappable. * #477: **New feature**: Add support for RFC 7662 (IntrospectTokenView, introspect scope) * **Compatibility**: Django 1.10 is the new minimum required version * **Compatibility**: Django 1.11 is now supported * **Backwards-incompatible**: The `oauth2_provider.ext.rest_framework` module has been moved to `oauth2_provider.contrib.rest_framework` * #177: Changed `id` field on Application, AccessToken, RefreshToken and Grant to BigAutoField (bigint/bigserial) * #321: Added `created` and `updated` auto fields to Application, AccessToken, RefreshToken and Grant * #476: Disallow empty redirect URIs * Fixed bad `url` parameter in some error responses. * Django 2.0 compatibility fixes. * The dependency on django-braces has been dropped. * The oauthlib dependency is no longer pinned. ### 0.12.0 [2017-02-24] * **New feature**: Class-based scopes backends. Listing scopes, available scopes and default scopes is now done through the class that the `SCOPES_BACKEND_CLASS` setting points to. By default, this is set to `oauth2_provider.scopes.SettingsScopes` which implements the legacy settings-based scope behaviour. No changes are necessary. * **Dropped support for Python 3.2 and Python 3.3**, added support for Python 3.6 * Support for the `scopes` query parameter, deprecated in 0.6.1, has been dropped * #448: Added support for customizing applications' allowed grant types * #141: The `is_usable(request)` method on the Application model can be overridden to dynamically enable or disable applications. * #434: Relax URL patterns to allow for UUID primary keys ### 0.11.0 [2016-12-1] * #315: AuthorizationView does not overwrite requests on get * #425: Added support for Django 1.10 * #396: added an IsAuthenticatedOrTokenHasScope Permission * #357: Support multiple-user clients by allowing User to be NULL for Applications * #389: Reuse refresh tokens if enabled. ### 0.10.0 [2015-12-14] * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** * #310: Fixed error that could occur sometimes when checking validity of incomplete AccessToken/Grant * #333: Added possibility to specify the default list of scopes returned when scope parameter is missing * #325: Added management views of issued tokens * #249: Added a command to clean expired tokens * #323: Application registration view uses custom application model in form class * #299: `server_class` is now pluggable through Django settings * #309: Add the py35-django19 env to travis * #308: Use compact syntax for tox envs * #306: Django 1.9 compatibility * #288: Put additional information when generating token responses * #297: Fixed doc about SessionAuthenticationMiddleware * #273: Generic read write scope by resource ### 0.9.0 [2015-07-28] * ``oauthlib_backend_class`` is now pluggable through Django settings * #127: ``application/json`` Content-Type is now supported using ``JSONOAuthLibCore`` * #238: Fixed redirect uri handling in case of error * #229: Invalidate access tokens when getting a new refresh token * added support for oauthlib 1.0 ### 0.8.2 [2015-06-25] * Fix the migrations to be two-step and allow upgrade from 0.7.2 ### 0.8.1 [2015-04-27] * South migrations fixed. Added new django migrations. ### 0.8.0 [2015-03-27] * Several docs improvements and minor fixes * #185: fixed vulnerabilities on Basic authentication * #173: ProtectResourceMixin now allows OPTIONS requests * Fixed `client_id` and `client_secret` characters set * #169: hide sensitive informations in error emails * #161: extend search to all token types when revoking a token * #160: return empty response on successful token revocation * #157: skip authorization form with ``skip_authorization_completely`` class field * #155: allow custom uri schemes * fixed ``get_application_model`` on Django 1.7 * fixed non rotating refresh tokens * #137: fixed base template * customized ``client_secret`` length * #38: create access tokens not bound to a user instance for *client credentials* flow ### 0.7.2 [2014-07-02] * Don't pin oauthlib ### 0.7.1 [2014-04-27] * Added database indexes to the OAuth2 related models to improve performances. **Warning: schema migration does not work for sqlite3 database, migration should be performed manually** ### 0.7.0 [2014-03-01] * Created a setting for the default value for approval prompt. * Improved docs * Don't pin django-braces and six versions **Backwards incompatible changes in 0.7.0** * Make Application model truly "swappable" (introduces a new non-namespaced setting `OAUTH2_PROVIDER_APPLICATION_MODEL`) ### 0.6.1 [2014-02-05] * added support for `scope` query parameter keeping backwards compatibility for the original `scopes` parameter. * __str__ method in Application model returns content of `name` field when available ### 0.6.0 [2014-01-26] * oauthlib 0.6.1 support * Django dev branch support * Python 2.6 support * Skip authorization form via `approval_prompt` parameter **Bugfixes** * Several fixes to the docs * Issue #71: Fix migrations * Issue #65: Use OAuth2 password grant with multiple devices * Issue #84: Add information about login template to tutorial. * Issue #64: Fix urlencode clientid secret ### 0.5.0 [2013-09-17] * oauthlib 0.6.0 support **Backwards incompatible changes in 0.5.0** * `backends.py` module has been renamed to `oauth2_backends.py` so you should change your imports whether you're extending this module **Bugfixes** * Issue #54: Auth backend proposal to address #50 * Issue #61: Fix contributing page * Issue #55: Add support for authenticating confidential client with request body params * Issue #53: Quote characters in the url query that are safe for Django but not for oauthlib ### 0.4.1 [2013-09-06] * Optimize queries on access token validation ### 0.4.0 [2013-08-09] **New Features** * Add Application management views, you no more need the admin to register, update and delete your application. * Add support to configurable application model * Add support for function based views **Backwards incompatible changes in 0.4.0** * `SCOPE` attribute in settings is now a dictionary to store `{'scope_name': 'scope_description'}` * Namespace `oauth2_provider` is mandatory in urls. See issue #36 **Bugfixes** * Issue #25: Bug in the Basic Auth parsing in Oauth2RequestValidator * Issue #24: Avoid generation of `client_id` with ":" colon char when using HTTP Basic Auth * Issue #21: IndexError when trying to authorize an application * Issue #9: `default_redirect_uri` is mandatory when `grant_type` is implicit, `authorization_code` or all-in-one * Issue #22: Scopes need a verbose description * Issue #33: Add django-oauth-toolkit version on example main page * Issue #36: Add mandatory namespace to urls * Issue #31: Add docstring to OAuthToolkitError and FatalClientError * Issue #32: Add docstring to `validate_uris` * Issue #34: Documentation tutorial part1 needs corsheaders explanation * Issue #36: Add mandatory namespace to urls * Issue #45: Add docs for AbstractApplication * Issue #47: Add docs for views decorators ### 0.3.2 [2013-07-10] * Bugfix #37: Error in migrations with custom user on Django 1.5 ### 0.3.1 [2013-07-10] * Bugfix #27: OAuthlib refresh token refactoring ### 0.3.0 [2013-06-14] * [Django REST Framework](http://django-rest-framework.org/) integration layer * Bugfix #13: Populate request with client and user in `validate_bearer_token` * Bugfix #12: Fix paths in documentation **Backwards incompatible changes in 0.3.0** * `requested_scopes` parameter in ScopedResourceMixin changed to `required_scopes` ### 0.2.1 [2013-06-06] * Core optimizations ### 0.2.0 [2013-06-05] * Add support for Django1.4 and Django1.6 * Add support for Python 3.3 * Add a default ReadWriteScoped view * Add tutorial to docs ### 0.1.0 [2013-05-31] * Support OAuth2 Authorization Flows ### 0.0.0 [2013-05-17] * Discussion with Daniel Greenfeld at Django Circus * Ignition django-oauth-toolkit-1.0.0/CONTRIBUTING.md000066400000000000000000000003411311555211500200010ustar00rootroot00000000000000# Contribute to Django OAuth Toolkit Thanks for your interest, we love contributions! Please [follow these guidelines](https://django-oauth-toolkit.readthedocs.io/en/latest/contributing.html) when submitting pull requests. django-oauth-toolkit-1.0.0/LICENSE000066400000000000000000000030301311555211500165530ustar00rootroot00000000000000Copyright (c) 2013, Massimiliano Pippi, Federico Frenguelli and contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. django-oauth-toolkit-1.0.0/MANIFEST.in000066400000000000000000000001161311555211500173060ustar00rootroot00000000000000include README.rst LICENSE recursive-include oauth2_provider/templates *.html django-oauth-toolkit-1.0.0/README.rst000066400000000000000000000060141311555211500172420ustar00rootroot00000000000000Django OAuth Toolkit ==================== *OAuth2 goodies for the Djangonauts!* .. image:: https://badge.fury.io/py/django-oauth-toolkit.png :target: http://badge.fury.io/py/django-oauth-toolkit .. image:: https://travis-ci.org/evonove/django-oauth-toolkit.png :alt: Build Status :target: https://travis-ci.org/evonove/django-oauth-toolkit .. image:: https://coveralls.io/repos/evonove/django-oauth-toolkit/badge.png :alt: Coverage Status :target: https://coveralls.io/r/evonove/django-oauth-toolkit If you are facing one or more of the following: * Your Django app exposes a web API you want to protect with OAuth2 authentication, * You need to implement an OAuth2 authorization server to provide tokens management for your infrastructure, Django OAuth Toolkit can help you providing out of the box all the endpoints, data and logic needed to add OAuth2 capabilities to your Django projects. Django OAuth Toolkit makes extensive use of the excellent `OAuthLib `_, so that everything is `rfc-compliant `_. Support ------- If you need support please send a message to the `Django OAuth Toolkit Google Group `_ Contributing ------------ We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the guidelines `_ and submit a PR. Reporting security issues ------------------------- If you believe you've found an issue with security implications, please send a detailed description via email to **security@evonove.it**. Mail sent to that address reaches the Django OAuth Toolkit core team, who can solve (or forward) the security issue as soon as possible. After our acknowledge, we may decide to open a public discussion in our mailing list or issues tracker. Once you’ve submitted an issue via email, you should receive a response from the core team within 48 hours, and depending on the action to be taken, you may receive further followup emails. Requirements ------------ * Python 2.7, 3.4, 3.5, 3.6 * Django 1.10, 1.11 Installation ------------ Install with pip:: pip install django-oauth-toolkit Add `oauth2_provider` to your `INSTALLED_APPS` .. code-block:: python INSTALLED_APPS = ( ... 'oauth2_provider', ) If you need an OAuth2 provider you'll want to add the following to your urls.py. Notice that `oauth2_provider` namespace is mandatory. .. code-block:: python urlpatterns = [ ... url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] Changelog --------- See `CHANGELOG.md `_. Documentation -------------- The `full documentation `_ is on *Read the Docs*. License ------- django-oauth-toolkit is released under the terms of the **BSD license**. Full details in ``LICENSE`` file. django-oauth-toolkit-1.0.0/docs/000077500000000000000000000000001311555211500165025ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/docs/Makefile000066400000000000000000000152321311555211500201450ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(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/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoOAuthToolkit.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoOAuthToolkit.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoOAuthToolkit" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoOAuthToolkit" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." django-oauth-toolkit-1.0.0/docs/advanced_topics.rst000066400000000000000000000111511311555211500223610ustar00rootroot00000000000000Advanced topics +++++++++++++++ .. _extend_app_model: Extending the Application model =============================== An Application instance represents a :term:`Client` on the :term:`Authorization server`. Usually an Application is issued to client's developers after they log in on an Authorization Server and pass in some data which identify the Application itself (let's say, the application name). Django OAuth Toolkit provides a very basic implementation of the Application model containing only the data strictly required during all the OAuth processes but you will likely need some extra info, like application logo, acceptance of some user agreement and so on. .. class:: AbstractApplication(models.Model) This is the base class implementing the bare minimum for Django OAuth Toolkit to work * :attr:`client_id` The client identifier issued to the client during the registration process as described in :rfc:`2.2` * :attr:`user` ref to a Django user * :attr:`redirect_uris` The list of allowed redirect uri. The string consists of valid URLs separated by space * :attr:`client_type` Client type as described in :rfc:`2.1` * :attr:`authorization_grant_type` Authorization flows available to the Application * :attr:`client_secret` Confidential secret issued to the client during the registration process as described in :rfc:`2.2` * :attr:`name` Friendly name for the Application Django OAuth Toolkit lets you extend the AbstractApplication model in a fashion like Django's custom user models. If you need, let's say, application logo and user agreement acceptance field, you can to this in your Django app (provided that your app is in the list of the INSTALLED_APPS in your settings module):: from django.db import models from oauth2_provider.models import AbstractApplication class MyApplication(AbstractApplication): logo = models.ImageField() agree = models.BooleanField() Then you need to tell Django OAuth Toolkit which model you want to use to represent applications. Write something like this in your settings module:: OAUTH2_PROVIDER_APPLICATION_MODEL='your_app_name.MyApplication' Be aware that, when you intend to swap the application model, you should create and run the migration defining the swapped application model prior to setting OAUTH2_PROVIDER_APPLICATION_MODEL. You'll run into models.E022 in Core system checks if you don't get the order right. That's all, now Django OAuth Toolkit will use your model wherever an Application instance is needed. **Notice:** `OAUTH2_PROVIDER_APPLICATION_MODEL` is the only setting variable that is not namespaced, this is because of the way Django currently implements swappable models. See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) for details Multiple Grants ~~~~~~~~~~~~~~~ The default application model supports a single OAuth grant (e.g. authorization code, client credentials). If you need applications to support multiple grants, override the `allows_grant_type` method. For example, if you want applications to support the authorization code *and* client credentials grants, you might do the following:: from oauth2_provider.models import AbstractApplication class MyApplication(AbstractApplication): def allows_grant_type(self, *grant_types): # Assume, for this example, that self.authorization_grant_type is set to self.GRANT_AUTHORIZATION_CODE return bool( set(self.authorization_grant_type, self.GRANT_CLIENT_CREDENTIALS) & grant_types ) .. _skip-auth-form: Skip authorization form ======================= Depending on the OAuth2 flow in use and the access token policy, users might be prompted for the same authorization multiple times: sometimes this is acceptable or even desirable but other times it isn't. To control DOT behaviour you can use the `approval_prompt` parameter when hitting the authorization endpoint. Possible values are: * `force` - users are always prompted for authorization. * `auto` - users are prompted only the first time, subsequent authorizations for the same application and scopes will be automatically accepted. Skip authorization completely for trusted applications ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You might want to completely bypass the authorization form, for instance if your application is an in-house product or if you already trust the application owner by other means. To this end, you have to set ``skip_authorization = True`` on the ``Application`` model, either programmaticaly or within the Django admin. Users will *not* be prompted for authorization, even on the first use of the application. django-oauth-toolkit-1.0.0/docs/changelog.rst000066400000000000000000000163111311555211500211650ustar00rootroot00000000000000Changelog ========= 0.12.0 [2017-02-24] ------------------- * **New feature**: Class-based scopes backends. Listing scopes, available scopes and default scopes is now done through the class that the `SCOPES_BACKEND_CLASS` setting points to. By default, this is set to `oauth2_provider.scopes.SettingsScopes` which implements the legacy settings-based scope behaviour. No changes are necessary. * **Dropped support for Python 3.2 and Python 3.3**, added support for Python 3.6 * Support for the `scopes` query parameter, deprecated in 0.6.1, has been dropped * #448: Added support for customizing applications' allowed grant types * #141: The `is_usable(request)` method on the Application model can be overridden to dynamically enable or disable applications. * #434: Relax URL patterns to allow for UUID primary keys 0.11.0 [2016-12-1] ------------------ * #424: Added a ROTATE_REFRESH_TOKEN setting to control whether refresh tokens are reused or not * #315: AuthorizationView does not overwrite requests on get * #425: Added support for Django 1.10 * #396: Added an IsAuthenticatedOrTokenHasScope Permission * #357: Support multiple-user clients by allowing User to be NULL for Applications * #389: Reuse refresh tokens if enabled. 0.10.0 [2015-12-14] ------------------- * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** * #310: Fixed error that could occur sometimes when checking validity of incomplete AccessToken/Grant * #333: Added possibility to specify the default list of scopes returned when scope parameter is missing * #325: Added management views of issued tokens * #249: Added a command to clean expired tokens * #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings * #309: Add the py35-django19 env to travis * #308: Use compact syntax for tox envs * #306: Django 1.9 compatibility * #288: Put additional information when generating token responses * #297: Fixed doc about SessionAuthenticationMiddleware * #273: Generic read write scope by resource 0.9.0 [2015-07-28] ------------------ * ``oauthlib_backend_class`` is now pluggable through Django settings * #127: ``application/json`` Content-Type is now supported using ``JSONOAuthLibCore`` * #238: Fixed redirect uri handling in case of error * #229: Invalidate access tokens when getting a new refresh token * added support for oauthlib 1.0 0.8.2 [2015-06-25] ------------------ * Fix the migrations to be two-step and allow upgrade from 0.7.2 0.8.1 [2015-04-27] ------------------ * South migrations fixed. Added new django migrations. 0.8.0 [2015-03-27] ------------------ * Several docs improvements and minor fixes * #185: fixed vulnerabilities on Basic authentication * #173: ProtectResourceMixin now allows OPTIONS requests * Fixed client_id and client_secret characters set * #169: hide sensitive informations in error emails * #161: extend search to all token types when revoking a token * #160: return empty response on successful token revocation * #157: skip authorization form with ``skip_authorization_completely`` class field * #155: allow custom uri schemes * fixed ``get_application_model`` on Django 1.7 * fixed non rotating refresh tokens * #137: fixed base template * customized ``client_secret`` lenght * #38: create access tokens not bound to a user instance for *client credentials* flow 0.7.2 [2014-07-02] ------------------ * Don't pin oauthlib 0.7.0 [2014-03-01] ------------------ * Created a setting for the default value for approval prompt. * Improved docs * Don't pin django-braces and six versions **Backwards incompatible changes in 0.7.0** * Make Application model truly "swappable" (introduces a new non-namespaced setting OAUTH2_PROVIDER_APPLICATION_MODEL) 0.6.1 [2014-02-05] ------------------ * added support for `scope` query parameter keeping backwards compatibility for the original `scopes` parameter. * __str__ method in Application model returns name when available 0.6.0 [2014-01-26] ------------------ * oauthlib 0.6.1 support * Django dev branch support * Python 2.6 support * Skip authorization form via `approval_prompt` parameter **Bugfixes** * Several fixes to the docs * Issue #71: Fix migrations * Issue #65: Use OAuth2 password grant with multiple devices * Issue #84: Add information about login template to tutorial. * Issue #64: Fix urlencode clientid secret 0.5.0 [2013-09-17] ------------------ * oauthlib 0.6.0 support **Backwards incompatible changes in 0.5.0** * backends.py module has been renamed to oauth2_backends.py so you should change your imports whether you're extending this module **Bugfixes** * Issue #54: Auth backend proposal to address #50 * Issue #61: Fix contributing page * Issue #55: Add support for authenticating confidential client with request body params * Issue #53: Quote characters in the url query that are safe for Django but not for oauthlib 0.4.1 [2013-09-06] ------------------ * Optimize queries on access token validation 0.4.0 [2013-08-09] ------------------ **New Features** * Add Application management views, you no more need the admin to register, update and delete your application. * Add support to configurable application model * Add support for function based views **Backwards incompatible changes in 0.4.0** * `SCOPE` attribute in settings is now a dictionary to store `{'scope_name': 'scope_description'}` * Namespace 'oauth2_provider' is mandatory in urls. See issue #36 **Bugfixes** * Issue #25: Bug in the Basic Auth parsing in Oauth2RequestValidator * Issue #24: Avoid generation of client_id with ":" colon char when using HTTP Basic Auth * Issue #21: IndexError when trying to authorize an application * Issue #9: Default_redirect_uri is mandatory when grant_type is implicit, authorization_code or all-in-one * Issue #22: Scopes need a verbose description * Issue #33: Add django-oauth-toolkit version on example main page * Issue #36: Add mandatory namespace to urls * Issue #31: Add docstring to OAuthToolkitError and FatalClientError * Issue #32: Add docstring to validate_uris * Issue #34: Documentation tutorial part1 needs corsheaders explanation * Issue #36: Add mandatory namespace to urls * Issue #45: Add docs for AbstractApplication * Issue #47: Add docs for views decorators 0.3.2 [2013-07-10] ------------------ * Bugfix #37: Error in migrations with custom user on Django 1.5 0.3.1 [2013-07-10] ------------------ * Bugfix #27: OAuthlib refresh token refactoring 0.3.0 [2013-06-14] ------------------ * `Django REST Framework `_ integration layer * Bugfix #13: Populate request with client and user in validate_bearer_token * Bugfix #12: Fix paths in documentation **Backwards incompatible changes in 0.3.0** * `requested_scopes` parameter in ScopedResourceMixin changed to `required_scopes` 0.2.1 [2013-06-06] ------------------ * Core optimizations 0.2.0 [2013-06-05] ------------------ * Add support for Django1.4 and Django1.6 * Add support for Python 3.3 * Add a default ReadWriteScoped view * Add tutorial to docs 0.1.0 [2013-05-31] ------------------ * Support OAuth2 Authorization Flows 0.0.0 [2013-05-17] ------------------ * Discussion with Daniel Greenfeld at Django Circus * Ignition django-oauth-toolkit-1.0.0/docs/conf.py000066400000000000000000000205771311555211500200140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Django OAuth Toolkit documentation build configuration file, created by # sphinx-quickstart on Mon May 20 19:40:43 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os, re # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. here = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, here) sys.path.insert(0, os.path.dirname(here)) os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" import django django.setup() import oauth2_provider # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'rfc',] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Django OAuth Toolkit' copyright = u'2013, Evonove' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = oauth2_provider.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # http://www.sphinx-doc.org/en/1.5.1/ext/intersphinx.html extensions.append('sphinx.ext.intersphinx') intersphinx_mapping = {'python3': ('https://docs.python.org/3.6', None), 'django': ('http://django.readthedocs.org/en/latest/', None)} # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'classic' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'DjangoOAuthToolkitdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'DjangoOAuthToolkit.tex', u'Django OAuth Toolkit Documentation', u'Evonove', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'djangooauthtoolkit', u'Django OAuth Toolkit Documentation', [u'Evonove'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'DjangoOAuthToolkit', u'Django OAuth Toolkit Documentation', u'Evonove', 'DjangoOAuthToolkit', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False django-oauth-toolkit-1.0.0/docs/contributing.rst000066400000000000000000000106521311555211500217470ustar00rootroot00000000000000============ Contributing ============ Setup ===== Fork `django-oauth-toolkit` repository on `GitHub `_ and follow these steps: * Create a virtualenv and activate it * Clone your repository locally * cd into the repository and type `pip install -r requirements/optional.txt` (this will install both optional and base requirements, useful during development) Issues ====== You can find the list of bugs, enhancements and feature requests on the `issue tracker `_. If you want to fix an issue, pick up one and add a comment stating you're working on it. If the resolution implies a discussion or if you realize the comments on the issue are growing pretty fast, move the discussion to the `Google Group `_. Pull requests ============= Please avoid providing a pull request from your `master` and use **topic branches** instead; you can add as many commits as you want but please keep them in one branch which aims to solve one single issue. Then submit your pull request. To create a topic branch, simply do:: git checkout -b fix-that-issue Switched to a new branch 'fix-that-issue' When you're ready to submit your pull request, first push the topic branch to your GitHub repo:: git push origin fix-that-issue Now you can go to your repository dashboard on GitHub and open a pull request starting from your topic branch. You can apply your pull request to the `master` branch of django-oauth-toolkit (this should be the default behaviour of GitHub user interface). Next you should add a comment about your branch, and if the pull request refers to a certain issue, insert a link to it. The repo managers will be notified of your pull request and it will be reviewed, in the meantime you can continue to add commits to your topic branch (and push them up to GitHub) either if you see something that needs changing, or in response to a reviewer's comments. If a reviewer asks for changes, you do not need to close the pull and reissue it after making changes. Just make the changes locally, push them to GitHub, then add a comment to the discussion section of the pull request. Pull upstream changes into your fork regularly ============================================== It's a good practice to pull upstream changes from master into your fork on a regular basis, in fact if you work on outdated code and your changes diverge too far from master, the pull request has to be rejected. To pull in upstream changes:: git remote add upstream https://github.com/evonove/django-oauth-toolkit.git git fetch upstream Then merge the changes that you fetched:: git merge upstream/master For more info, see http://help.github.com/fork-a-repo/ .. note:: Please be sure to rebase your commits on the master when possible, so your commits can be fast-forwarded: we try to avoid *merge commits* when they are not necessary. How to get your pull request accepted ===================================== We really want your code, so please follow these simple guidelines to make the process as smooth as possible. Run the tests! -------------- Django OAuth Toolkit aims to support different Python and Django versions, so we use **tox** to run tests on multiple configurations. At any time during the development and at least before submitting the pull request, please run the testsuite via:: tox The first thing the core committers will do is run this command. Any pull request that fails this test suite will be **immediately rejected**. Add the tests! -------------- Whenever you add code, you have to add tests as well. We cannot accept untested code, so unless it is a peculiar situation you previously discussed with the core committers, if your pull request reduces the test coverage it will be **immediately rejected**. Code conventions matter ----------------------- There are no good nor bad conventions, just follow PEP8 (run some lint tool for this) and nobody will argue. Try reading our code and grasp the overall philosophy regarding method and variable names, avoid *black magics* for the sake of readability, keep in mind that *simple is better than complex*. If you feel the code is not straightforward, add a comment. If you think a function is not trivial, add a docstrings. The contents of this page are heavily based on the docs from `django-admin2 `_ django-oauth-toolkit-1.0.0/docs/glossary.rst000066400000000000000000000040731311555211500211030ustar00rootroot00000000000000Glossary ======== .. Put definition of specific terms here, and reference them inside docs with :term:`My term` syntax .. glossary:: Authorization Server The authorization server asks resource owners for their consensus to let client applications access their data. It also manages and issues the tokens needed for all the authorization flows supported by OAuth2 spec. Usually the same application offering resources through an OAuth2-protected API also behaves like an authorization server. Resource Server An application providing access to its own resources through an API protected following the OAuth2 spec. Application An Application represents a Client on the Authorization server. Usually an Application is created manually by client's developers after logging in on an Authorization Server. Client A client is an application authorized to access OAuth2-protected resources on behalf and with the authorization of the resource owner. Resource Owner The user of an application which exposes resources to third party applications through OAuth2. The resource owner must give her authorization for third party applications to be able to access her data. Access Token A token needed to access resources protected by OAuth2. It has a lifetime which is usually quite short. Authorization Code The authorization code is obtained by using an authorization server as an intermediary between the client and resource owner. It is used to authenticate the client and grant the transmission of the Access Token. Authorization Token A token the authorization server issues to clients that can be swapped for an access token. It has a very short lifetime since the swap has to be performed shortly after users provide their authorization. Refresh Token A token the authorization server may issue to clients and can be swapped for a brand new access token, without repeating the authorization process. It has no expire time.django-oauth-toolkit-1.0.0/docs/index.rst000066400000000000000000000025131311555211500203440ustar00rootroot00000000000000.. Django OAuth Toolkit documentation master file, created by sphinx-quickstart on Mon May 20 19:40:43 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Django OAuth Toolkit Documentation ============================================= Django OAuth Toolkit can help you providing out of the box all the endpoints, data and logic needed to add OAuth2 capabilities to your Django projects. Django OAuth Toolkit makes extensive use of the excellent `OAuthLib `_, so that everything is `rfc-compliant `_. See our :doc:`Changelog ` for information on updates. Support ------- If you need support please send a message to the `Django OAuth Toolkit Google Group `_ Requirements ------------ * Python 2.7, 3.4, 3.5, 3.6 * Django 1.8, 1.9, 1.10, 1.11 Index ===== .. toctree:: :maxdepth: 2 install tutorial/tutorial rest-framework/rest-framework views/views templates views/details models advanced_topics settings resource_server management_commands glossary .. toctree:: :maxdepth: 1 contributing changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` django-oauth-toolkit-1.0.0/docs/install.rst000066400000000000000000000011601311555211500207000ustar00rootroot00000000000000Installation ============ Install with pip :: pip install django-oauth-toolkit Add `oauth2_provider` to your `INSTALLED_APPS` .. code-block:: python INSTALLED_APPS = ( ... 'oauth2_provider', ) If you need an OAuth2 provider you'll want to add the following to your urls.py .. code-block:: python urlpatterns = [ ... url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] Sync your database ------------------ .. sourcecode:: sh $ python manage.py migrate oauth2_provider Next step is our :doc:`first tutorial `. django-oauth-toolkit-1.0.0/docs/management_commands.rst000066400000000000000000000015631311555211500232360ustar00rootroot00000000000000Management commands =================== Django OAuth Toolkit exposes some useful management commands that can be run via shell or by other means (eg: cron) .. _cleartokens: cleartokens ~~~~~~~~~~~ The ``cleartokens`` management command allows the user to remove those refresh tokens whose lifetime is greater than the amount specified by ``REFRESH_TOKEN_EXPIRE_SECONDS`` settings. It is important that this command is run regularly (eg: via cron) to avoid cluttering the database with expired refresh tokens. If ``cleartokens`` runs daily the maximum delay before a refresh token is removed is ``REFRESH_TOKEN_EXPIRE_SECONDS`` + 1 day. This is normally not a problem since refresh tokens are long lived. Note: Refresh tokens need to expire before AccessTokens can be removed from the database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect. django-oauth-toolkit-1.0.0/docs/models.rst000066400000000000000000000001101311555211500205070ustar00rootroot00000000000000`Models` ======== .. automodule:: oauth2_provider.models :members: django-oauth-toolkit-1.0.0/docs/resource_server.rst000066400000000000000000000042751311555211500224610ustar00rootroot00000000000000Separate Resource Server ======================== Django OAuth Toolkit allows to separate the :term:`Authentication Server` and the :term:`Resource Server.` Based one the `RFC 7662 `_ Django OAuth Toolkit provides a rfc-compliant introspection endpoint. As well the Django OAuth Toolkit allows to verify access tokens by the use of an introspection endpoint. Setup the Authentication Server ------------------------------- Setup the :term:`Authentication Server` as described in the :ref:`tutorial`. Create a OAuth2 access token for the :term:`Resource Server` and add the ``introspection``-Scope to the settings. .. code-block:: python 'SCOPES': { 'read': 'Read scope', 'write': 'Write scope', 'introspection': 'Introspect token scope', ... }, The :term:`Authentication Server` will listen for introspection requests. The endpoint is located within the ``oauth2_provider.urls`` as ``/introspect/``. Example Request:: POST /o/introspect/ HTTP/1.1 Host: server.example.com Accept: application/json Content-Type: application/x-www-form-urlencoded Authorization: Bearer 3yUqsWtwKYKHnfivFcJu token=uH3Po4KXWP4dsY4zgyxH Example Response:: HTTP/1.1 200 OK Content-Type: application/json { "active": true, "client_id": "oUdofn7rfhRtKWbmhyVk", "username": "jdoe", "scope": "read write dolphin", "exp": 1419356238 } Setup the Resource Server ------------------------- Setup the :term:`Resource Server` like the :term:`Authentication Server` as described in the :ref:`tutorial`. Add ``RESOURCE_SERVER_INTROSPECTION_URL`` and ``RESOURCE_SERVER_AUTH_TOKEN`` to your settings. The :term:`Resource Server` will try to verify its requests on the :term:`Authentication Server`. .. code-block:: python OAUTH2_PROVIDER = { ... 'RESOURCE_SERVER_INTROSPECTION_URL': 'https://example.org/o/introspect/', 'RESOURCE_SERVER_AUTH_TOKEN': '3yUqsWtwKYKHnfivFcJu', ... } ``RESOURCE_SERVER_INTROSPECTION_URL`` defines the introspection endpoint and ``RESOURCE_SERVER_AUTH_TOKEN`` an authentication token to authenticate against the :term:`Authentication Server`. django-oauth-toolkit-1.0.0/docs/rest-framework/000077500000000000000000000000001311555211500214525ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/docs/rest-framework/getting_started.rst000066400000000000000000000156221311555211500254010ustar00rootroot00000000000000Getting started =============== Django OAuth Toolkit provide a support layer for `Django REST Framework `_. This tutorial is based on the Django REST Framework example and shows you how to easily integrate with it. **NOTE** The following code has been tested with django 1.7.7 and Django REST Framework 3.1.1 Step 1: Minimal setup --------------------- Create a virtualenv and install following packages using `pip`... :: pip install django-oauth-toolkit djangorestframework Start a new Django project and add `'rest_framework'` and `'oauth2_provider'` to your `INSTALLED_APPS` setting. .. code-block:: python INSTALLED_APPS = ( 'django.contrib.admin', ... 'oauth2_provider', 'rest_framework', ) Now we need to tell Django REST Framework to use the new authentication backend. To do so add the following lines at the end of your `settings.py` module: .. code-block:: python REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', ) } Step 2: Create a simple API --------------------------- Let's create a simple API for accessing users and groups. Here's our project's root `urls.py` module: .. code-block:: python from django.conf.urls import url, include from django.contrib.auth.models import User, Group from django.contrib import admin admin.autodiscover() from rest_framework import permissions, routers, serializers, viewsets from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope # first we define the serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ("username", "email", "first_name", "last_name", ) class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group fields = ("name", ) # ViewSets define the view behavior. class UserViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope] queryset = User.objects.all() serializer_class = UserSerializer class GroupViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated, TokenHasScope] required_scopes = ['groups'] queryset = Group.objects.all() serializer_class = GroupSerializer # Routers provide an easy way of automatically determining the URL conf router = routers.DefaultRouter() router.register(r'users', UserViewSet) router.register(r'groups', GroupViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browseable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), # ... ] Also add the following to your `settings.py` module: .. code-block:: python OAUTH2_PROVIDER = { # this is the list of available scopes 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'} } REST_FRAMEWORK = { # ... 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ) } `OAUTH2_PROVIDER.SCOPES` setting parameter contains the scopes that the application will be aware of, so we can use them for permission check. Now run the following commands: :: python manage.py migrate python manage.py createsuperuser python manage.py runserver The first command creates the tables, the second creates the admin user account and the last one runs the application. Next thing you should do is to login in the admin at :: http://localhost:8000/admin and create some users and groups that will be queried later through our API. Step 3: Register an application ------------------------------- To obtain a valid access_token first we must register an application. DOT has a set of customizable views you can use to CRUD application instances, just point your browser at: :: http://localhost:8000/o/applications/ Click on the link to create a new application and fill the form with the following data: * Name: *just a name of your choice* * Client Type: *confidential* * Authorization Grant Type: *Resource owner password-based* Save your app! Step 4: Get your token and use your API --------------------------------------- At this point we're ready to request an access_token. Open your shell :: curl -X POST -d "grant_type=password&username=&password=" -u":" http://localhost:8000/o/token/ The *user_name* and *password* are the credential of the users registered in your :term:`Authorization Server`, like any user created in Step 2. Response should be something like: .. code-block:: javascript { "access_token": "", "token_type": "Bearer", "expires_in": 36000, "refresh_token": "", "scope": "read write groups" } Grab your access_token and start using your new OAuth2 API: :: # Retrieve users curl -H "Authorization: Bearer " http://localhost:8000/users/ curl -H "Authorization: Bearer " http://localhost:8000/users/1/ # Retrieve groups curl -H "Authorization: Bearer " http://localhost:8000/groups/ # Insert a new user curl -H "Authorization: Bearer " -X POST -d"username=foo&password=bar" http://localhost:8000/users/ Step 5: Testing Restricted Access --------------------------------- Let's try to access resources using a token with a restricted scope adding a `scope` parameter to the token request :: curl -X POST -d "grant_type=password&username=&password=&scope=read" -u":" http://localhost:8000/o/token/ As you can see the only scope provided is `read`: .. code-block:: javascript { "access_token": "", "token_type": "Bearer", "expires_in": 36000, "refresh_token": "", "scope": "read" } We now try to access our resources: :: # Retrieve users curl -H "Authorization: Bearer " http://localhost:8000/users/ curl -H "Authorization: Bearer " http://localhost:8000/users/1/ Ok, this one works since users read only requires `read` scope. :: # 'groups' scope needed curl -H "Authorization: Bearer " http://localhost:8000/groups/ # 'write' scope needed curl -H "Authorization: Bearer " -X POST -d"username=foo&password=bar" http://localhost:8000/users/ You'll get a `"You do not have permission to perform this action"` error because your access_token does not provide the required scopes `groups` and `write`. django-oauth-toolkit-1.0.0/docs/rest-framework/permissions.rst000066400000000000000000000072661311555211500245720ustar00rootroot00000000000000Permissions =========== Django OAuth Toolkit provides a few utility classes to use along with other permissions in Django REST Framework, so you can easily add scoped-based permission checks to your API views. More details on how to add custom permissions to your API Endpoints can be found at the official `Django REST Framework documentation `_ TokenHasScope ------------- The `TokenHasScope` permission class allows the access only when the current access token has been authorized for **all** the scopes listed in the `required_scopes` field of the view. For example: .. code-block:: python class SongView(views.APIView): authentication_classes = [OAuth2Authentication] permission_classes = [TokenHasScope] required_scopes = ['music'] The `required_scopes` attribute is mandatory. TokenHasReadWriteScope ---------------------- The `TokenHasReadWriteScope` permission class allows the access based on the `READ_SCOPE` and `WRITE_SCOPE` configured in the settings. When the current request's method is one of the "safe" methods `GET`, `HEAD`, `OPTIONS` the access is allowed only if the access token has been authorized for the `READ_SCOPE` scope. When the request's method is one of `POST`, `PUT`, `PATCH`, `DELETE` the access is allowed if the access token has been authorized for the `WRITE_SCOPE`. The `required_scopes` attribute is optional and can be used to other scopes needed by the view. For example: .. code-block:: python class SongView(views.APIView): authentication_classes = [OAuth2Authentication] permission_classes = [TokenHasReadWriteScope] required_scopes = ['music'] When a request is performed both the `READ_SCOPE` \\ `WRITE_SCOPE` and 'music' scopes are required to be authorized for the current access token. TokenHasResourceScope ---------------------- The `TokenHasResourceScope` permission class allows the access only when the current access token has been authorized for **all** the scopes listed in the `required_scopes` field of the view but according of request's method. When the current request's method is one of the "safe" methods, the access is allowed only if the access token has been authorized for the `scope:read` scope (for example `music:read`). When the request's method is one of "non safe" methods, the access is allowed only if the access token has been authorizes for the `scope:write` scope (for example `music:write`). .. code-block:: python class SongView(views.APIView): authentication_classes = [OAuth2Authentication] permission_classes = [TokenHasResourceScope] required_scopes = ['music'] The `required_scopes` attribute is mandatory (you just need inform the resource scope). IsAuthenticatedOrTokenHasScope ------------------------------ The `TokenHasResourceScope` permission class allows the access only when the current access token has been authorized for **all** the scopes listed in the `required_scopes` field of the view but according of request's method. And also allows access to Authenticated users who are authenticated in django, but were not authenticated trought the OAuth2Authentication class. This allows for protection of the api using scopes, but still let's users browse the full browseable API. To restrict users to only browse the parts of the browseable API they should be allowed to see, you can combine this with the DjangoModelPermission or the DjangoObjectPermission. For example: .. code-block:: python class SongView(views.APIView): permission_classes = [IsAuthenticatedOrTokenHasScope, DjangoModelPermission] required_scopes = ['music'] The `required_scopes` attribute is mandatory. django-oauth-toolkit-1.0.0/docs/rest-framework/rest-framework.rst000066400000000000000000000001601311555211500251510ustar00rootroot00000000000000Django Rest Framework --------------------- .. toctree:: :maxdepth: 2 getting_started permissions django-oauth-toolkit-1.0.0/docs/rfc.py000066400000000000000000000021071311555211500176260ustar00rootroot00000000000000""" Custom Sphinx documentation module to link to parts of the OAuth2 RFC. """ from docutils import nodes base_url = "http://tools.ietf.org/html/rfc6749" def rfclink(name, rawtext, text, lineno, inliner, options={}, content=[]): """Link to the OAuth2 draft. Returns 2 part tuple containing list of nodes to insert into the document and a list of system messages. Both are allowed to be empty. :param name: The role name used in the document. :param rawtext: The entire markup snippet, with role. :param text: The text marked with the role. :param lineno: The line number where rawtext appears in the input. :param inliner: The inliner instance that called us. :param options: Directive options for customization. :param content: The directive content for customization. """ node = nodes.reference(rawtext, "RFC6749 Section " + text, refuri="%s#section-%s" % (base_url, text)) return [node], [] def setup(app): """ Install the plugin. :param app: Sphinx application context. """ app.add_role("rfc", rfclink) django-oauth-toolkit-1.0.0/docs/settings.rst000066400000000000000000000131201311555211500210710ustar00rootroot00000000000000Settings ======== Our configurations are all namespaced under the `OAUTH2_PROVIDER` settings with the exception of `OAUTH2_PROVIDER_APPLICATION_MODEL, OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL, OAUTH2_PROVIDER_GRANT_MODEL, OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL`: this is because of the way Django currently implements swappable models. See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) for details. For example: .. code-block:: python OAUTH2_PROVIDER = { 'SCOPES': { 'read': 'Read scope', 'write': 'Write scope', }, 'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator', } A big *thank you* to the guys from Django REST Framework for inspiring this. List of available settings -------------------------- ACCESS_TOKEN_EXPIRE_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The number of seconds an access token remains valid. Requesting a protected resource after this duration will fail. Keep this value high enough so clients can cache the token for a reasonable amount of time. ACCESS_TOKEN_MODEL ~~~~~~~~~~~~~~~~~~ The import string of the class (model) representing your access tokens. Overwrite this value if you wrote your own implementation (subclass of ``oauth2_provider.models.AccessToken``). ALLOWED_REDIRECT_URI_SCHEMES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``["http", "https"]`` A list of schemes that the ``redirect_uri`` field will be validated against. Setting this to ``["https"]`` only in production is strongly recommended. APPLICATION_MODEL ~~~~~~~~~~~~~~~~~ The import string of the class (model) representing your applications. Overwrite this value if you wrote your own implementation (subclass of ``oauth2_provider.models.Application``). AUTHORIZATION_CODE_EXPIRE_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The number of seconds an authorization code remains valid. Requesting an access token after this duration will fail. :rfc:`4.1.2` recommends a 10 minutes (600 seconds) duration. CLIENT_ID_GENERATOR_CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~ The import string of the class responsible for generating client identifiers. These are usually random strings. CLIENT_SECRET_GENERATOR_CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The import string of the class responsible for generating client secrets. These are usually random strings. CLIENT_SECRET_GENERATOR_LENGTH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The length of the generated secrets, in characters. If this value is too low, secrets may become subject to bruteforce guessing. GRANT_MODEL ~~~~~~~~~~~~~~~~~ The import string of the class (model) representing your grants. Overwrite this value if you wrote your own implementation (subclass of ``oauth2_provider.models.Grant``). OAUTH2_SERVER_CLASS ~~~~~~~~~~~~~~~~~~~ The import string for the ``server_class`` (or ``oauthlib.oauth2.Server`` subclass) used in the ``OAuthLibMixin`` that implements OAuth2 grant types. OAUTH2_VALIDATOR_CLASS ~~~~~~~~~~~~~~~~~~~~~~ The import string of the ``oauthlib.oauth2.RequestValidator`` subclass that validates every step of the OAuth2 process. OAUTH2_BACKEND_CLASS ~~~~~~~~~~~~~~~~~~~~ The import string for the ``oauthlib_backend_class`` used in the ``OAuthLibMixin``, to get a ``Server`` instance. REFRESH_TOKEN_EXPIRE_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The number of seconds before a refresh token gets removed from the database by the ``cleartokens`` management command. Check :ref:`cleartokens` management command for further info. REFRESH_TOKEN_MODEL ~~~~~~~~~~~~~~~~~~~ The import string of the class (model) representing your refresh tokens. Overwrite this value if you wrote your own implementation (subclass of ``oauth2_provider.models.RefreshToken``). ROTATE_REFRESH_TOKEN ~~~~~~~~~~~~~~~~~~~~ When is set to `True` (default) a new refresh token is issued to the client when the client refreshes an access token. REQUEST_APPROVAL_PROMPT ~~~~~~~~~~~~~~~~~~~~~~~ Can be ``'force'`` or ``'auto'``. The strategy used to display the authorization form. Refer to :ref:`skip-auth-form`. SCOPES_BACKEND_CLASS ~~~~~~~~~~~~~~~~~~~~ **New in 0.12.0**. The import string for the scopes backend class. Defaults to , which reads scopes through the settings defined below. SCOPES ~~~~~~ .. note:: (0.12.0+) Only used if `SCOPES_BACKEND_CLASS` is set to the SettingsScopes default. A dictionary mapping each scope name to its human description. .. _settings_default_scopes: DEFAULT_SCOPES ~~~~~~~~~~~~~~ .. note:: (0.12.0+) Only used if `SCOPES_BACKEND_CLASS` is set to the SettingsScopes default. A list of scopes that should be returned by default. This is a subset of the keys of the SCOPES setting. By default this is set to '__all__' meaning that the whole set of SCOPES will be returned. .. code-block:: python DEFAULT_SCOPES = ['read', 'write'] READ_SCOPE ~~~~~~~~~~ .. note:: (0.12.0+) Only used if `SCOPES_BACKEND_CLASS` is set to the SettingsScopes default. The name of the *read* scope. WRITE_SCOPE ~~~~~~~~~~~ .. note:: (0.12.0+) Only used if `SCOPES_BACKEND_CLASS` is set to the SettingsScopes default. The name of the *write* scope. RESOURCE_SERVER_INTROSPECTION_URL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The introspection endpoint for validating token remotely (RFC7662). RESOURCE_SERVER_AUTH_TOKEN ~~~~~~~~~~~~~~~~~~~~~~~~~~ The bearer token to authenticate the introspection request towards the introspection endpoint (RFC7662). RESOURCE_SERVER_TOKEN_CACHING_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The number of seconds an authorization token received from the introspection endpoint remains valid. If the expire time of the received token is less than ``RESOURCE_SERVER_TOKEN_CACHING_SECONDS`` the expire time will be used. django-oauth-toolkit-1.0.0/docs/templates.rst000066400000000000000000000215351311555211500212400ustar00rootroot00000000000000Templates ========= A set of templates is provided. These templates range from Django Admin Site alternatives to manage the Apps that use your App as a provider, to Error and Authorization Templates. You can override default templates located in ``templates/oauth2_provider`` folder and provide a custom layout. To override these templates you just need to create a folder named ``oauth2_provider`` inside your templates folder and, inside this folder, add a file that matches the name of the template you're trying to override. .. important: In ``INSTALLED_APPS`` on ``settings.py``, ``'django.contrib.staticfiles'``, must be before ``'oauth2_provider'``. .. note: Every view provides access only to data belonging to the logged in user who performs the request. The templates available are: - `base.html`_ - `authorize.html`_ - `Management`_: - `Application`_: - `application_list.html`_ - `application_form.html`_ - `application_registration_form.html`_ - `application_detail.html`_ - `application_confirm_delete.html`_ - `Token`_: - `authorized-tokens.html`_ - `authorized-token-delete.html`_ base.html --------- If you just want a different look and feel you may only override this template. To inherit this template just add ``{% extends "oauth2_provider/base.html" %}`` in the first line of the other templates. This is what is done with the default templates. The blocks defined in it are: - ``title`` inside the HTML title tag; - ``css`` inside the head; - ``content`` in the body. .. note: See ` Django docs on template inheritance `_ for more information on the use of blocks. authorize.html -------------- Authorize is rendered in :class:`~oauth2_provider.views.base.AuthorizationView` (``authorize/``). This template gets passed the following context variables: - ``scopes`` - :obj:`list` with the scopes requested by the application; .. caution:: See :ref:`settings_default_scopes` to understand what is returned if no scopes are requested. - ``scopes_descriptions`` - :obj:`list` with the descriptions for the scopes requested; - ``application`` - An :class:`~oauth2_provider.models.Application` object .. note:: If you haven't created your own Application Model (see how in :ref:`extend_app_model`), you will get an :class:`~oauth2_provider.models.AbstractApplication` object. - ``client_id`` - Passed in the URI, already validated. - ``redirect_uri`` - Passed in the URI (optional), already validated. .. note:: If it wasn't provided on the request, the default one has been set (see :meth:`~oauth2_provider.models.AbstractApplication.default_redirect_uri`). - ``response_type`` - Passed in the URI, already validated. - ``state`` - Passed in the URI (optional). - ``form`` - An :class:`~oauth2_provider.forms.AllowForm` with all the hidden fields already filled with the values above. .. important:: One extra variable, named ``error`` will also be available if an Oauth2 exception occurs. This variable is a :obj:`dict` with ``error`` and ``description`` Example (this is the default page you may find on ``templates/oauth2_provider/authorize.html``): :: {% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}
{% if not error %}

{% trans "Authorize" %} {{ application.name }}?

{% csrf_token %} {% for field in form %} {% if field.is_hidden %} {{ field }} {% endif %} {% endfor %}

{% trans "Application requires following permissions" %}

    {% for scope in scopes_descriptions %}
  • {{ scope }}
  • {% endfor %}
{{ form.errors }} {{ form.non_field_errors }}
{% else %}

Error: {{ error.error }}

{{ error.description }}

{% endif %}
{% endblock %} Management ---------- The management templates are Django Admin Site alternatives to manage the Apps. Application ``````````` All templates receive :class:`~oauth2_provider.models.Application` objects. .. note:: If you haven't created your own Application Model (see how in :ref:`extend_app_model`), you will get an :class:`~oauth2_provider.models.AbstractApplication` object. application_list.html ~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.ApplicationList` (``applications/``). This class inherits :class:`django.views.generic.edit.ListView`. This template gets passed the following template context variable: - ``applications`` - a :obj:`list` with all the applications, may be ``None``. application_form.html ~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.ApplicationUpdate` (``applications//update/``). This class inherits :class:`django.views.generic.edit.UpdateView`. This template gets passed the following template context variables: - ``application`` - the :class:`~oauth2_provider.models.Application` object. - ``form`` - a :obj:`~django.forms.Form` with the following fields: - ``name`` - ``client_id`` - ``client_secret`` - ``client_type`` - ``authorization_grant_type`` - ``redirect_uris`` .. caution:: In the default implementation this template in extended by `application_registration_form.html`_. Be sure to provide the same blocks if you are only overiding this template. application_registration_form.html ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.ApplicationRegistration` (``applications/register/``). This class inherits :class:`django.views.generic.edit.CreateView`. This template gets passed the following template context variable: - ``form`` - a :obj:`~django.forms.Form` with the following fields: - ``name`` - ``client_id`` - ``client_secret`` - ``client_type`` - ``authorization_grant_type`` - ``redirect_uris`` .. note:: In the default implementation this template extends `application_form.html`_. application_detail.html ~~~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.ApplicationDetail` (``applications//``). This class inherits :class:`django.views.generic.edit.DetailView`. This template gets passed the following template context variable: - ``application`` - the :class:`~oauth2_provider.models.Application` object. application_confirm_delete.html ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.ApplicationDelete` (``applications//delete/``). This class inherits :class:`django.views.generic.edit.DeleteView`. This template gets passed the following template context variable: - ``application`` - the :class:`~oauth2_provider.models.Application` object. .. important:: To override successfully this template you should provide a form that posts to the same URL, example: ``
`` Token ````` All templates receive :class:`~oauth2_provider.models.AccessToken` objects. authorized-tokens.html ~~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.AuthorizedTokensListView` (``authorized_tokens/``). This class inherits :class:`django.views.generic.edit.ListView`. This template gets passed the following template context variable: - ``authorized_tokens`` - a :obj:`list` with all the tokens that belong to applications that the user owns, may be ``None``. .. important:: To override successfully this template you should provide links to revoke the token, example: ``revoke`` authorized-token-delete.html ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Rendered in :class:`~oauth2_provider.views.base.AuthorizedTokenDeleteView` (``authorized_tokens//delete/``). This class inherits :class:`django.views.generic.edit.DeleteView`. This template gets passed the following template context variable: - ``authorized_token`` - the :class:`~oauth2_provider.models.AccessToken` object. .. important:: To override successfully this template you should provide a form that posts to the same URL, example: ````django-oauth-toolkit-1.0.0/docs/tutorial/000077500000000000000000000000001311555211500203455ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/docs/tutorial/tutorial.rst000066400000000000000000000001571311555211500227450ustar00rootroot00000000000000Tutorials ========= .. toctree:: :maxdepth: 2 tutorial_01 tutorial_02 tutorial_03 tutorial_04 django-oauth-toolkit-1.0.0/docs/tutorial/tutorial_01.rst000066400000000000000000000160021311555211500232410ustar00rootroot00000000000000Part 1 - Make a Provider in a Minute ==================================== Scenario -------- You want to make your own :term:`Authorization Server` to issue access tokens to client applications for a certain API. Start Your App -------------- During this tutorial you will make an XHR POST from a Heroku deployed app to your localhost instance. Since the domain that will originate the request (the app on Heroku) is different from the destination domain (your local instance), you will need to install the `django-cors-middleware `_ app. These "cross-domain" requests are by default forbidden by web browsers unless you use `CORS `_. Create a virtualenv and install `django-oauth-toolkit` and `django-cors-middleware`: :: pip install django-oauth-toolkit django-cors-middleware Start a Django project, add `oauth2_provider` and `corsheaders` to the installed apps, and enable admin: .. code-block:: python INSTALLED_APPS = { 'django.contrib.admin', # ... 'oauth2_provider', 'corsheaders', } Include the Django OAuth Toolkit urls in your `urls.py`, choosing the urlspace you prefer. For example: .. code-block:: python urlpatterns = [ url(r"^admin/", admin.site.urls)), url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), # ... ] Include the CORS middleware in your `settings.py`: .. code-block:: python MIDDLEWARE = ( # ... 'corsheaders.middleware.CorsMiddleware', # ... ) # Or on Django < 1.10: MIDDLEWARE_CLASSES = ( # ... 'corsheaders.middleware.CorsMiddleware', # ... ) Allow CORS requests from all domains (just for the scope of this tutorial): .. code-block:: python CORS_ORIGIN_ALLOW_ALL = True .. _loginTemplate: Include the required hidden input in your login template, `registration/login.html`. The ``{{ next }}`` template context variable will be populated with the correct redirect value. See the `Django documentation `_ for details on using login templates. .. code-block:: html As a final step, execute the migrate command, start the internal server, and login with your credentials. Create an OAuth2 Client Application ----------------------------------- Before your :term:`Application` can use the :term:`Authorization Server` for user login, you must first register the app (also known as the :term:`Client`.) Once registered, your app will be granted access to the API, subject to approval by its users. Let's register your application. Point your browser to http://localhost:8000/o/applications/ and add an Application instance. `Client id` and `Client Secret` are automatically generated; you have to provide the rest of the informations: * `User`: the owner of the Application (e.g. a developer, or the currently logged in user.) * `Redirect uris`: Applications must register at least one redirection endpoint before using the authorization endpoint. The :term:`Authorization Server` will deliver the access token to the client only if the client specifies one of the verified redirection uris. For this tutorial, paste verbatim the value `http://django-oauth-toolkit.herokuapp.com/consumer/exchange/` * `Client type`: this value affects the security level at which some communications between the client application and the authorization server are performed. For this tutorial choose *Confidential*. * `Authorization grant type`: choose *Authorization code* * `Name`: this is the name of the client application on the server, and will be displayed on the authorization request page, where users can allow/deny access to their data. Take note of the `Client id` and the `Client Secret` then logout (this is needed only for testing the authorization process we'll explain shortly) Test Your Authorization Server ------------------------------ Your authorization server is ready and can begin issuing access tokens. To test the process you need an OAuth2 consumer; if you are familiar enough with OAuth2, you can use curl, requests, or anything that speaks http. For the rest of us, there is a `consumer service `_ deployed on Heroku to test your provider. Build an Authorization Link for Your Users ++++++++++++++++++++++++++++++++++++++++++ Authorizing an application to access OAuth2 protected data in an :term:`Authorization Code` flow is always initiated by the user. Your application can prompt users to click a special link to start the process. Go to the `Consumer `_ page and complete the form by filling in your application's details obtained from the steps in this tutorial. Submit the form, and you'll receive a link your users can use to access the authorization page. Authorize the Application +++++++++++++++++++++++++ When a user clicks the link, she is redirected to your (possibly local) :term:`Authorization Server`. If you're not logged in, you will be prompted for username and password. This is because the authorization page is login protected by django-oauth-toolkit. Login, then you should see the (not so cute) form a user can use to give her authorization to the client application. Flag the *Allow* checkbox and click *Authorize*, you will be redirected again to the consumer service. __ loginTemplate_ If you are not redirected to the correct page after logging in successfully, you probably need to `setup your login template correctly`__. Exchange the token ++++++++++++++++++ At this point your authorization server redirected the user to a special page on the consumer passing in an :term:`Authorization Code`, a special token the consumer will use to obtain the final access token. This operation is usually done automatically by the client application during the request/response cycle, but we cannot make a POST request from Heroku to your localhost, so we proceed manually with this step. Fill the form with the missing data and click *Submit*. If everything is ok, you will be routed to another page showing your access token, the token type, its lifetime and the :term:`Refresh Token`. Refresh the token +++++++++++++++++ The page showing the access token retrieved from the :term:`Authorization Server` also let you make a POST request to the server itself to swap the refresh token for another, brand new access token. Just fill in the missing form fields and click the Refresh button: if everything goes smoothly you will see the access and refresh token change their values, otherwise you will likely see an error message. When you have finished playing with your authorization server, take note of both the access and refresh tokens, we will use them for the next part of the tutorial. So let's make an API and protect it with your OAuth2 tokens in the :doc:`part 2 of the tutorial `. django-oauth-toolkit-1.0.0/docs/tutorial/tutorial_02.rst000066400000000000000000000105551311555211500232510ustar00rootroot00000000000000Part 2 - protect your APIs ========================== Scenario -------- It's very common for an :term:`Authorization Server` being also the :term:`Resource Server`, usually exposing an API to let others access its own resources. Django OAuth Toolkit implements an easy way to protect the views of a Django application with OAuth2, in this tutorial we will see how to do it. Make your API ------------- We start where we left the :doc:`part 1 of the tutorial `: you have an authorization server and we want it to provide an API to access some kind of resources. We don't need an actual resource, so we will simply expose an endpoint protected with OAuth2: let's do it in a *class based view* fashion! Django OAuth Toolkit provides a set of generic class based view you can use to add OAuth behaviour to your views. Open your `views.py` module and import the view: .. code-block:: python from oauth2_provider.views.generic import ProtectedResourceView from django.http import HttpResponse Then create the view which will respond to the API endpoint: .. code-block:: python class ApiEndpoint(ProtectedResourceView): def get(self, request, *args, **kwargs): return HttpResponse('Hello, OAuth2!') That's it, our API will expose only one method, responding to `GET` requests. Now open your `urls.py` and specify the URL this view will respond to: .. code-block:: python from django.conf.urls import url, include import oauth2_provider.views as oauth2_views from django.conf import settings from .views import ApiEndpoint # OAuth2 provider endpoints oauth2_endpoint_views = [ url(r'^authorize/$', oauth2_views.AuthorizationView.as_view(), name="authorize"), url(r'^token/$', oauth2_views.TokenView.as_view(), name="token"), url(r'^revoke-token/$', oauth2_views.RevokeTokenView.as_view(), name="revoke-token"), ] if settings.DEBUG: # OAuth2 Application Management endpoints oauth2_endpoint_views += [ url(r'^applications/$', oauth2_views.ApplicationList.as_view(), name="list"), url(r'^applications/register/$', oauth2_views.ApplicationRegistration.as_view(), name="register"), url(r'^applications/(?P\d+)/$', oauth2_views.ApplicationDetail.as_view(), name="detail"), url(r'^applications/(?P\d+)/delete/$', oauth2_views.ApplicationDelete.as_view(), name="delete"), url(r'^applications/(?P\d+)/update/$', oauth2_views.ApplicationUpdate.as_view(), name="update"), ] # OAuth2 Token Management endpoints oauth2_endpoint_views += [ url(r'^authorized-tokens/$', oauth2_views.AuthorizedTokensListView.as_view(), name="authorized-token-list"), url(r'^authorized-tokens/(?P\d+)/delete/$', oauth2_views.AuthorizedTokenDeleteView.as_view(), name="authorized-token-delete"), ] urlpatterns = [ # OAuth 2 endpoints: url(r'^o/', include(oauth2_endpoint_views, namespace="oauth2_provider")), url(r'^api/hello', ApiEndpoint.as_view()), # an example resource endpoint ] You will probably want to write your own application views to deal with permissions and access control but the ones packaged with the library can get you started when developing the app. Since we inherit from `ProtectedResourceView`, we're done and our API is OAuth2 protected - for the sake of the lazy programmer. Testing your API ---------------- Time to make requests to your API. For a quick test, try accessing your app at the url `/api/hello` with your browser and verify that it responds with a `403` (in fact no `HTTP_AUTHORIZATION` header was provided). You can test your API with anything that can perform HTTP requests, but for this tutorial you can use the online `consumer client `_. Just fill the form with the URL of the API endpoint (i.e. http://localhost:8000/api/hello if you're on localhost) and the access token coming from the :doc:`part 1 of the tutorial `. Going in the Django admin and get the token from there is not considered cheating, so it's an option. Try performing a request and check that your :term:`Resource Server` aka :term:`Authorization Server` correctly responds with an HTTP 200. :doc:`Part 3 of the tutorial ` will show how to use an access token to authenticate users. django-oauth-toolkit-1.0.0/docs/tutorial/tutorial_03.rst000066400000000000000000000066751311555211500232620ustar00rootroot00000000000000Part 3 - OAuth2 token authentication ==================================== Scenario -------- You want to use an :term:`Access Token` to authenticate users against Django's authentication system. Setup a provider ---------------- You need a fully-functional OAuth2 provider which is able to release access tokens: just follow the steps in :doc:`the part 1 of the tutorial `. To enable OAuth2 token authentication you need a middleware that checks for tokens inside requests and a custom authentication backend which takes care of token verification. In your settings.py: .. code-block:: python AUTHENTICATION_BACKENDS = ( 'oauth2_provider.backends.OAuth2Backend', # Uncomment following if you want to access the admin #'django.contrib.auth.backends.ModelBackend' '...', ) MIDDLEWARE = ( '...', # If you use SessionAuthenticationMiddleware, be sure it appears before OAuth2TokenMiddleware. # SessionAuthenticationMiddleware is NOT required for using django-oauth-toolkit. 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', '...', ) # Or on Django<1.10: MIDDLEWARE_CLASSES = ( '...', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', '...', ) You will likely use the `django.contrib.auth.backends.ModelBackend` along with the OAuth2 backend (or you might not be able to log in into the admin), only pay attention to the order in which Django processes authentication backends. If you put the OAuth2 backend *after* the AuthenticationMiddleware and `request.user` is valid, the backend will do nothing; if `request.user` is the Anonymous user it will try to authenticate the user using the OAuth2 access token. If you put the OAuth2 backend *before* AuthenticationMiddleware, or AuthenticationMiddleware is not used at all, it will try to authenticate user with the OAuth2 access token and set `request.user` and `request._cached_user` fields so that AuthenticationMiddleware (when active) will not try to get user from the session. If you use SessionAuthenticationMiddleware, be sure it appears before OAuth2TokenMiddleware. However SessionAuthenticationMiddleware is NOT required for using django-oauth-toolkit. Protect your view ----------------- The authentication backend will run smoothly with, for example, `login_required` decorators, so that you can have a view like this in your `views.py` module: .. code-block:: python from django.contrib.auth.decorators import login_required from django.http.response import HttpResponse @login_required() def secret_page(request, *args, **kwargs): return HttpResponse('Secret contents!', status=200) To check everything works properly, mount the view above to some url: .. code-block:: python urlpatterns = [ url(r'^secret$', 'my.views.secret_page', name='secret'), '...', ] You should have an :term:`Application` registered at this point, if you don't, follow the steps in the previous tutorials to create one. Obtain an :term:`Access Token`, either following the OAuth2 flow of your application or manually creating in the Django admin. Now supposing your access token value is `123456` you can try to access your authenticated view: :: curl -H "Authorization: Bearer 123456" -X GET http://localhost:8000/secret django-oauth-toolkit-1.0.0/docs/tutorial/tutorial_04.rst000066400000000000000000000044141311555211500232500ustar00rootroot00000000000000Part 4 - Revoking an OAuth2 Token ================================= Scenario -------- You've granted a user an :term:`Access Token`, following :doc:`part 1 ` and now you would like to revoke that token, probably in response to a client request (to logout). Revoking a Token ---------------- Be sure that you've granted a valid token. If you've hooked in `oauth-toolkit` into your `urls.py` as specified in :doc:`part 1 `, you'll have a URL at `/o/revoke_token`. By submitting the appropriate request to that URL, you can revoke a user's :term:`Access Token`. `Oauthlib `_ is compliant with https://tools.ietf.org/html/rfc7009, so as specified, the revocation request requires: - token: REQUIRED, this is the :term:`Access Token` you want to revoke - token_type_hint: OPTIONAL, designating either 'access_token' or 'refresh_token'. Note that these revocation-specific parameters are in addition to the authentication parameters already specified by your particular client type. Setup a Request --------------- Depending on the client type you're using, the token revocation request you may submit to the authentication server may vary. A `Public` client, for example, will not have access to your `Client Secret`. A revoke request from a public client would omit that secret, and take the form: :: POST /o/revoke_token/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded token=XXXX&client_id=XXXX Where token is :term:`Access Token` specified above, and client_id is the `Client id` obtained in obtained in :doc:`part 1 `. If your application type is `Confidential` , it requires a `Client secret`, you will have to add it as one of the parameters: :: POST /o/revoke_token/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded token=XXXX&client_id=XXXX&client_secret=XXXX The server will respond wih a `200` status code on successful revocation. You can use `curl` to make a revoke request on your server. If you have access to a local installation of your authorization server, you can test revoking a token with a request like that shown below, for a `Confidential` client. :: curl --data "token=XXXX&client_id=XXXX&client_secret=XXXX" http://localhost:8000/o/revoke_token/ django-oauth-toolkit-1.0.0/docs/views/000077500000000000000000000000001311555211500176375ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/docs/views/application.rst000066400000000000000000000010341311555211500226720ustar00rootroot00000000000000Application Views ================= A set of views is provided to let users handle application instances without accessing Django Admin Site. Application views are listed at the url `applications/` and you can register a new one at the url `applications/register`. You can override default templates located in `templates/oauth2_provider` folder and provide a custom layout. Every view provides access only to data belonging to the logged in user who performs the request. .. automodule:: oauth2_provider.views.application :members: django-oauth-toolkit-1.0.0/docs/views/class_based.rst000066400000000000000000000045461311555211500226450ustar00rootroot00000000000000Class-based Views ================= Django OAuth Toolkit provides generic classes useful to implement OAuth2 protected endpoints using the *Class Based View* approach. .. class:: ProtectedResourceView(ProtectedResourceMixin, View): A view that provides OAuth2 authentication out of the box. To implement a protected endpoint, just define your CBV as:: class MyEndpoint(ProtectedResourceView): """ A GET endpoint that needs OAuth2 authentication """ def get(self, request, *args, **kwargs): return HttpResponse('Hello, World!') **Please notice**: ``OPTION`` method is not OAuth2 protected to allow preflight requests. .. class:: ScopedProtectedResourceView(ScopedResourceMixin, ProtectedResourceView): A view that provides OAuth2 authentication and scopes handling out of the box. To implement a protected endpoint, just define your CBV specifying the ``required_scopes`` field:: class MyScopedEndpoint(ScopedProtectedResourceView): required_scopes = ['can_make_it can_break_it'] """ A GET endpoint that needs OAuth2 authentication and a set of scopes: 'can_make_it' and 'can_break_it' """ def get(self, request, *args, **kwargs): return HttpResponse('Hello, World!') .. class:: ReadWriteScopedResourceView(ReadWriteScopedResourceMixin, ProtectedResourceView): A view that provides OAuth2 authentication and read/write default scopes. ``GET``, ``HEAD``, ``OPTIONS`` http methods require ``read`` scope, others methods need the ``write`` scope. If you need, you can always specify an additional list of scopes in the ``required_scopes`` field:: class MyRWEndpoint(ReadWriteScopedResourceView): required_scopes = ['has_additional_powers'] # optional """ A GET endpoint that needs OAuth2 authentication and the 'read' scope. If required_scopes was specified, clients also need those scopes. """ def get(self, request, *args, **kwargs): return HttpResponse('Hello, World!') Generic views in DOT are obtained composing a set of mixins you can find in the :doc:`views.mixins ` module: feel free to use those mixins directly if you want to provide your own class based views. django-oauth-toolkit-1.0.0/docs/views/details.rst000066400000000000000000000012201311555211500220110ustar00rootroot00000000000000Views code and details ====================== Generic ------- Generic views are intended to use in a "batteries included" fashion to protect own views with OAuth2 authentication and Scopes handling. .. automodule:: oauth2_provider.views.generic :members: Mixins ------ These views are mainly for internal use, but advanced users may use them as basic components to customize OAuth2 logic inside their Django applications. .. automodule:: oauth2_provider.views.mixins :members: Base ---- Views needed to implement the main OAuth2 authorization flows supported by Django OAuth Toolkit. .. automodule:: oauth2_provider.views.base :members: django-oauth-toolkit-1.0.0/docs/views/function_based.rst000066400000000000000000000044521311555211500233610ustar00rootroot00000000000000Function-based views ==================== Django OAuth Toolkit provides decorators to help you in protecting your function-based views. .. function:: protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server) Decorator to protect views by providing OAuth2 authentication out of the box, optionally with scope handling. Basic usage, without using scopes:: from oauth2_provider.decorators import protected_resource @protected_resource() def my_view(request): # An access token is required to get here... # ... pass If you want to check scopes as well when accessing a view you can pass them along as decorator's parameter:: from oauth2_provider.decorators import protected_resource @protected_resource(scopes=['can_make_it can_break_it']) def my_view(request): # An access token AND the right scopes are required to get here... # ... pass The decorator also accept server and validator classes if you want or need to use your own OAuth2 logic:: from oauth2_provider.decorators import protected_resource from myapp.oauth2_validators import MyValidator @protected_resource(validator_cls=MyValidator) def my_view(request): # You have to leverage your own logic to get here... # ... pass .. function:: rw_protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server) Decorator to protect views by providing OAuth2 authentication and read/write scopes out of the box. GET, HEAD, OPTIONS http methods require "read" scope. Otherwise "write" scope is required:: from oauth2_provider.decorators import rw_protected_resource @rw_protected_resource() def my_view(request): # If this is a POST, you have to provide 'write' scope to get here... # ... pass If you need, you can ask for other scopes over "read" and "write":: from oauth2_provider.decorators import rw_protected_resource @rw_protected_resource(scopes=['exotic_scope']) def my_view(request): # If this is a POST, you have to provide 'exotic_scope write' scopes to get here... # ... pass django-oauth-toolkit-1.0.0/docs/views/mixins.rst000066400000000000000000000001641311555211500217010ustar00rootroot00000000000000Mixins for Class Based Views ============================ .. automodule:: oauth2_provider.views.mixins :members:django-oauth-toolkit-1.0.0/docs/views/token.rst000066400000000000000000000012611311555211500215110ustar00rootroot00000000000000Granted Tokens Views ==================== A set of views is provided to let users handle tokens that have been granted to them, without needing to accessing Django Admin Site. Every view provides access only to the tokens that have been granted to the user performing the request. Granted Token views are listed at the url `authorized_tokens/`. For each granted token there is a delete view that allows you to delete such token. You can override default templates `authorized-tokens.html` for the list view and `authorized-token-delete.html` for the delete view; they are located inside `templates/oauth2_provider` folder. .. automodule:: oauth2_provider.views.token :members: django-oauth-toolkit-1.0.0/docs/views/views.rst000066400000000000000000000003241311555211500215250ustar00rootroot00000000000000Using the views =============== Django OAuth Toolkit provides a set of pre-defined views for different purposes: .. toctree:: :maxdepth: 2 function_based class_based application token mixins django-oauth-toolkit-1.0.0/oauth2_provider/000077500000000000000000000000001311555211500206665ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/__init__.py000066400000000000000000000002251311555211500227760ustar00rootroot00000000000000import pkg_resources __version__ = pkg_resources.require("django-oauth-toolkit")[0].version default_app_config = "oauth2_provider.apps.DOTConfig" django-oauth-toolkit-1.0.0/oauth2_provider/admin.py000066400000000000000000000023711311555211500223330ustar00rootroot00000000000000from django.contrib import admin from .models import ( get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model, ) class ApplicationAdmin(admin.ModelAdmin): list_display = ("name", "user", "client_type", "authorization_grant_type") list_filter = ("client_type", "authorization_grant_type", "skip_authorization") radio_fields = { "client_type": admin.HORIZONTAL, "authorization_grant_type": admin.VERTICAL, } raw_id_fields = ("user", ) class GrantAdmin(admin.ModelAdmin): list_display = ("code", "application", "user", "expires") raw_id_fields = ("user", ) class AccessTokenAdmin(admin.ModelAdmin): list_display = ("token", "user", "application", "expires") raw_id_fields = ("user", ) class RefreshTokenAdmin(admin.ModelAdmin): list_display = ("token", "user", "application") raw_id_fields = ("user", "access_token") Application = get_application_model() Grant = get_grant_model() AccessToken = get_access_token_model() RefreshToken = get_refresh_token_model() admin.site.register(Application, ApplicationAdmin) admin.site.register(Grant, GrantAdmin) admin.site.register(AccessToken, AccessTokenAdmin) admin.site.register(RefreshToken, RefreshTokenAdmin) django-oauth-toolkit-1.0.0/oauth2_provider/apps.py000066400000000000000000000002071311555211500222020ustar00rootroot00000000000000from django.apps import AppConfig class DOTConfig(AppConfig): name = "oauth2_provider" verbose_name = "Django OAuth Toolkit" django-oauth-toolkit-1.0.0/oauth2_provider/backends.py000066400000000000000000000012141311555211500230100ustar00rootroot00000000000000from django.contrib.auth import get_user_model from .oauth2_backends import get_oauthlib_core UserModel = get_user_model() OAuthLibCore = get_oauthlib_core() class OAuth2Backend(object): """ Authenticate against an OAuth2 access token """ def authenticate(self, request=None, **credentials): if request is not None: valid, r = OAuthLibCore.verify_request(request, scopes=[]) if valid: return r.user return None def get_user(self, user_id): try: return UserModel.objects.get(pk=user_id) except UserModel.DoesNotExist: return None django-oauth-toolkit-1.0.0/oauth2_provider/compat.py000066400000000000000000000016511311555211500225260ustar00rootroot00000000000000""" The `compat` module provides support for backwards compatibility with older versions of django and python. """ # flake8: noqa from __future__ import unicode_literals # urlparse in python3 has been renamed to urllib.parse try: from urlparse import parse_qs, parse_qsl, urlparse, urlsplit, urlunparse, urlunsplit except ImportError: from urllib.parse import parse_qs, parse_qsl, urlparse, urlsplit, urlunsplit, urlunparse try: from urllib import urlencode, quote_plus, unquote_plus except ImportError: from urllib.parse import urlencode, quote_plus, unquote_plus # bastb Django 1.10 has updated Middleware. This code imports the Mixin required to get old-style # middleware working again # More? # https://docs.djangoproject.com/en/1.10/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware try: from django.utils.deprecation import MiddlewareMixin except ImportError: MiddlewareMixin = object django-oauth-toolkit-1.0.0/oauth2_provider/contrib/000077500000000000000000000000001311555211500223265ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/contrib/__init__.py000066400000000000000000000000001311555211500244250ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/contrib/rest_framework/000077500000000000000000000000001311555211500253605ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/contrib/rest_framework/__init__.py000066400000000000000000000003161311555211500274710ustar00rootroot00000000000000# flake8: noqa from .authentication import OAuth2Authentication from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope from .permissions import IsAuthenticatedOrTokenHasScope django-oauth-toolkit-1.0.0/oauth2_provider/contrib/rest_framework/authentication.py000066400000000000000000000015001311555211500307450ustar00rootroot00000000000000from rest_framework.authentication import BaseAuthentication from ...oauth2_backends import get_oauthlib_core class OAuth2Authentication(BaseAuthentication): """ OAuth 2 authentication backend using `django-oauth-toolkit` """ www_authenticate_realm = "api" def authenticate(self, request): """ Returns two-tuple of (user, token) if authentication succeeds, or None otherwise. """ oauthlib_core = get_oauthlib_core() valid, r = oauthlib_core.verify_request(request, scopes=[]) if valid: return r.user, r.access_token else: return None def authenticate_header(self, request): """ Bearer is the only finalized type currently """ return 'Bearer realm="%s"' % self.www_authenticate_realm django-oauth-toolkit-1.0.0/oauth2_provider/contrib/rest_framework/permissions.py000066400000000000000000000071201311555211500303050ustar00rootroot00000000000000import logging from django.core.exceptions import ImproperlyConfigured from rest_framework.permissions import BasePermission, IsAuthenticated from .authentication import OAuth2Authentication from ...settings import oauth2_settings log = logging.getLogger("oauth2_provider") SAFE_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"] class TokenHasScope(BasePermission): """ The request is authenticated as a user and the token used has the right scope """ def has_permission(self, request, view): token = request.auth if not token: return False if hasattr(token, "scope"): # OAuth 2 required_scopes = self.get_scopes(request, view) log.debug("Required scopes to access resource: {0}".format(required_scopes)) return token.is_valid(required_scopes) assert False, ("TokenHasScope requires the" "`oauth2_provider.rest_framework.OAuth2Authentication` authentication " "class to be used.") def get_scopes(self, request, view): try: return getattr(view, "required_scopes") except AttributeError: raise ImproperlyConfigured( "TokenHasScope requires the view to define the required_scopes attribute" ) class TokenHasReadWriteScope(TokenHasScope): """ The request is authenticated as a user and the token used has the right scope """ def get_scopes(self, request, view): try: required_scopes = super(TokenHasReadWriteScope, self).get_scopes(request, view) except ImproperlyConfigured: required_scopes = [] # TODO: code duplication!! see dispatch in ReadWriteScopedResourceMixin if request.method.upper() in SAFE_HTTP_METHODS: read_write_scope = oauth2_settings.READ_SCOPE else: read_write_scope = oauth2_settings.WRITE_SCOPE return required_scopes + [read_write_scope] class TokenHasResourceScope(TokenHasScope): """ The request is authenticated as a user and the token used has the right scope """ def get_scopes(self, request, view): try: view_scopes = ( super(TokenHasResourceScope, self).get_scopes(request, view) ) except ImproperlyConfigured: view_scopes = [] if request.method.upper() in SAFE_HTTP_METHODS: scope_type = oauth2_settings.READ_SCOPE else: scope_type = oauth2_settings.WRITE_SCOPE required_scopes = [ "{}:{}".format(scope, scope_type) for scope in view_scopes ] return required_scopes class IsAuthenticatedOrTokenHasScope(BasePermission): """ The user is authenticated using some backend or the token has the right scope This only returns True if the user is authenticated, but not using a token or using a token, and the token has the correct scope. This is usefull when combined with the DjangoModelPermissions to allow people browse the browsable api's if they log in using the a non token bassed middleware, and let them access the api's using a rest client with a token """ def has_permission(self, request, view): is_authenticated = IsAuthenticated().has_permission(request, view) oauth2authenticated = False if is_authenticated: oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication) token_has_scope = TokenHasScope() return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view) django-oauth-toolkit-1.0.0/oauth2_provider/decorators.py000066400000000000000000000060551311555211500234130ustar00rootroot00000000000000from functools import wraps from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseForbidden from oauthlib.oauth2 import Server from .oauth2_backends import OAuthLibCore from .oauth2_validators import OAuth2Validator from .scopes import get_scopes_backend from .settings import oauth2_settings def protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server): """ Decorator to protect views by providing OAuth2 authentication out of the box, optionally with scope handling. @protected_resource() def my_view(request): # An access token is required to get here... # ... pass """ _scopes = scopes or [] def decorator(view_func): @wraps(view_func) def _validate(request, *args, **kwargs): validator = validator_cls() core = OAuthLibCore(server_cls(validator)) valid, oauthlib_req = core.verify_request(request, scopes=_scopes) if valid: request.resource_owner = oauthlib_req.user return view_func(request, *args, **kwargs) return HttpResponseForbidden() return _validate return decorator def rw_protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server): """ Decorator to protect views by providing OAuth2 authentication and read/write scopes out of the box. GET, HEAD, OPTIONS http methods require "read" scope. Otherwise "write" scope is required. @rw_protected_resource() def my_view(request): # If this is a POST, you have to provide 'write' scope to get here... # ... pass """ _scopes = scopes or [] def decorator(view_func): @wraps(view_func) def _validate(request, *args, **kwargs): # Check if provided scopes are acceptable provided_scopes = get_scopes_backend().get_all_scopes() read_write_scopes = [oauth2_settings.READ_SCOPE, oauth2_settings.WRITE_SCOPE] if not set(read_write_scopes).issubset(set(provided_scopes)): raise ImproperlyConfigured( "rw_protected_resource decorator requires following scopes {0}" " to be in OAUTH2_PROVIDER['SCOPES'] list in settings".format( read_write_scopes) ) # Check if method is safe if request.method.upper() in ["GET", "HEAD", "OPTIONS"]: _scopes.append(oauth2_settings.READ_SCOPE) else: _scopes.append(oauth2_settings.WRITE_SCOPE) # proceed with validation validator = validator_cls() core = OAuthLibCore(server_cls(validator)) valid, oauthlib_req = core.verify_request(request, scopes=_scopes) if valid: request.resource_owner = oauthlib_req.user return view_func(request, *args, **kwargs) return HttpResponseForbidden() return _validate return decorator django-oauth-toolkit-1.0.0/oauth2_provider/exceptions.py000066400000000000000000000006711311555211500234250ustar00rootroot00000000000000class OAuthToolkitError(Exception): """ Base class for exceptions """ def __init__(self, error=None, redirect_uri=None, *args, **kwargs): super(OAuthToolkitError, self).__init__(*args, **kwargs) self.oauthlib_error = error if redirect_uri: self.oauthlib_error.redirect_uri = redirect_uri class FatalClientError(OAuthToolkitError): """ Class for critical errors """ pass django-oauth-toolkit-1.0.0/oauth2_provider/forms.py000066400000000000000000000006421311555211500223700ustar00rootroot00000000000000from django import forms class AllowForm(forms.Form): allow = forms.BooleanField(required=False) redirect_uri = forms.CharField(widget=forms.HiddenInput()) scope = forms.CharField(widget=forms.HiddenInput()) client_id = forms.CharField(widget=forms.HiddenInput()) state = forms.CharField(required=False, widget=forms.HiddenInput()) response_type = forms.CharField(widget=forms.HiddenInput()) django-oauth-toolkit-1.0.0/oauth2_provider/generators.py000066400000000000000000000025601311555211500234140ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals from oauthlib.common import generate_client_id as oauthlib_generate_client_id from oauthlib.common import UNICODE_ASCII_CHARACTER_SET from .settings import oauth2_settings class BaseHashGenerator(object): """ All generators should extend this class overriding `.hash()` method. """ def hash(self): raise NotImplementedError() class ClientIdGenerator(BaseHashGenerator): def hash(self): """ Generate a client_id for Basic Authentication scheme without colon char as in http://tools.ietf.org/html/rfc2617#section-2 """ return oauthlib_generate_client_id(length=40, chars=UNICODE_ASCII_CHARACTER_SET) class ClientSecretGenerator(BaseHashGenerator): def hash(self): length = oauth2_settings.CLIENT_SECRET_GENERATOR_LENGTH chars = UNICODE_ASCII_CHARACTER_SET return oauthlib_generate_client_id(length=length, chars=chars) def generate_client_id(): """ Generate a suitable client id """ client_id_generator = oauth2_settings.CLIENT_ID_GENERATOR_CLASS() return client_id_generator.hash() def generate_client_secret(): """ Generate a suitable client secret """ client_secret_generator = oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS() return client_secret_generator.hash() django-oauth-toolkit-1.0.0/oauth2_provider/http.py000066400000000000000000000005411311555211500222170ustar00rootroot00000000000000from django.http import HttpResponseRedirect from .settings import oauth2_settings class HttpResponseUriRedirect(HttpResponseRedirect): def __init__(self, redirect_to, *args, **kwargs): self.allowed_schemes = oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES super(HttpResponseUriRedirect, self).__init__(redirect_to, *args, **kwargs) django-oauth-toolkit-1.0.0/oauth2_provider/management/000077500000000000000000000000001311555211500230025ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/management/__init__.py000066400000000000000000000000001311555211500251010ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/management/commands/000077500000000000000000000000001311555211500246035ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/management/commands/__init__.py000066400000000000000000000000001311555211500267020ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/management/commands/cleartokens.py000066400000000000000000000004051311555211500274660ustar00rootroot00000000000000from django.core.management.base import BaseCommand from ...models import clear_expired class Command(BaseCommand): help = "Can be run as a cronjob or directly to clean out expired tokens" def handle(self, *args, **options): clear_expired() django-oauth-toolkit-1.0.0/oauth2_provider/middleware.py000066400000000000000000000031211311555211500233520ustar00rootroot00000000000000from django.contrib.auth import authenticate from django.utils.cache import patch_vary_headers from .compat import MiddlewareMixin class OAuth2TokenMiddleware(MiddlewareMixin): """ Middleware for OAuth2 user authentication This middleware is able to work along with AuthenticationMiddleware and its behaviour depends on the order it's processed with. If it comes *after* AuthenticationMiddleware and request.user is valid, leave it as is and does not proceed with token validation. If request.user is the Anonymous user proceeds and try to authenticate the user using the OAuth2 access token. If it comes *before* AuthenticationMiddleware, or AuthenticationMiddleware is not used at all, tries to authenticate user with the OAuth2 access token and set request.user field. Setting also request._cached_user field makes AuthenticationMiddleware use that instead of the one from the session. It also adds "Authorization" to the "Vary" header, so that django's cache middleware or a reverse proxy can create proper cache keys. """ def process_request(self, request): # do something only if request contains a Bearer token if request.META.get("HTTP_AUTHORIZATION", "").startswith("Bearer"): if not hasattr(request, "user") or request.user.is_anonymous: user = authenticate(request=request) if user: request.user = request._cached_user = user def process_response(self, request, response): patch_vary_headers(response, ("Authorization",)) return response django-oauth-toolkit-1.0.0/oauth2_provider/migrations/000077500000000000000000000000001311555211500230425ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/migrations/0001_initial.py000066400000000000000000000106421311555211500255100ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from oauth2_provider.settings import oauth2_settings from django.db import models, migrations import oauth2_provider.validators import oauth2_provider.generators from django.conf import settings class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(oauth2_settings.APPLICATION_MODEL), migrations.swappable_dependency(oauth2_settings.ACCESS_TOKEN_MODEL), migrations.swappable_dependency(oauth2_settings.REFRESH_TOKEN_MODEL), migrations.swappable_dependency(oauth2_settings.GRANT_MODEL), ] operations = [ migrations.CreateModel( name='Application', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('client_id', models.CharField(default=oauth2_provider.generators.generate_client_id, unique=True, max_length=100, db_index=True)), ('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated', blank=True, validators=[oauth2_provider.validators.validate_uris])), ('client_type', models.CharField(max_length=32, choices=[('confidential', 'Confidential'), ('public', 'Public')])), ('authorization_grant_type', models.CharField(max_length=32, choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')])), ('client_secret', models.CharField(default=oauth2_provider.generators.generate_client_secret, max_length=255, db_index=True, blank=True)), ('name', models.CharField(max_length=255, blank=True)), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'abstract': False, 'swappable': 'OAUTH2_PROVIDER_APPLICATION_MODEL', }, ), migrations.CreateModel( name='AccessToken', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('token', models.CharField(max_length=255, db_index=True)), ('expires', models.DateTimeField()), ('scope', models.TextField(blank=True)), ('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'abstract': False, 'swappable': 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL', }, ), migrations.CreateModel( name='Grant', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('code', models.CharField(max_length=255, db_index=True)), ('expires', models.DateTimeField()), ('redirect_uri', models.CharField(max_length=255)), ('scope', models.TextField(blank=True)), ('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'abstract': False, 'swappable': 'OAUTH2_PROVIDER_GRANT_MODEL', }, ), migrations.CreateModel( name='RefreshToken', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('token', models.CharField(max_length=255, db_index=True)), ('access_token', models.OneToOneField(related_name='refresh_token', to=oauth2_settings.ACCESS_TOKEN_MODEL, on_delete=models.CASCADE)), ('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)), ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'abstract': False, 'swappable': 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL', }, ), ] django-oauth-toolkit-1.0.0/oauth2_provider/migrations/0002_08_updates.py000066400000000000000000000021651311555211500260350ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from oauth2_provider.settings import oauth2_settings from django.db import models, migrations import oauth2_provider.validators import oauth2_provider.generators from django.conf import settings class Migration(migrations.Migration): dependencies = [ ('oauth2_provider', '0001_initial'), ] operations = [ migrations.AddField( model_name='Application', name='skip_authorization', field=models.BooleanField(default=False), preserve_default=True, ), migrations.AlterField( model_name='Application', name='user', field=models.ForeignKey(related_name='oauth2_provider_application', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( model_name='AccessToken', name='user', field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), preserve_default=True, ), ] django-oauth-toolkit-1.0.0/oauth2_provider/migrations/0003_auto_20160316_1503.py000066400000000000000000000010421311555211500264550ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models from django.conf import settings class Migration(migrations.Migration): dependencies = [ ('oauth2_provider', '0002_08_updates'), ] operations = [ migrations.AlterField( model_name='application', name='user', field=models.ForeignKey(related_name='oauth2_provider_application', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), ), ] django-oauth-toolkit-1.0.0/oauth2_provider/migrations/0004_auto_20160525_1623.py000066400000000000000000000013741311555211500264730ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('oauth2_provider', '0003_auto_20160316_1503'), ] operations = [ migrations.AlterField( model_name='accesstoken', name='token', field=models.CharField(unique=True, max_length=255), ), migrations.AlterField( model_name='grant', name='code', field=models.CharField(unique=True, max_length=255), ), migrations.AlterField( model_name='refreshtoken', name='token', field=models.CharField(unique=True, max_length=255), ), ] django-oauth-toolkit-1.0.0/oauth2_provider/migrations/0005_auto_20170514_1141.py000066400000000000000000000072521311555211500264670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.11.1 on 2017-05-14 11:41 from __future__ import unicode_literals from oauth2_provider.settings import oauth2_settings from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('oauth2_provider', '0004_auto_20160525_1623'), ] operations = [ migrations.AlterField( model_name='accesstoken', name='application', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=oauth2_settings.APPLICATION_MODEL), ), migrations.AlterField( model_name='accesstoken', name='id', field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name='accesstoken', name='user', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_accesstoken', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='application', name='id', field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name='grant', name='id', field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name='grant', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_grant', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='refreshtoken', name='id', field=models.BigAutoField(primary_key=True, serialize=False), ), migrations.AlterField( model_name='refreshtoken', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_refreshtoken', to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='accesstoken', name='created', field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name='accesstoken', name='updated', field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name='application', name='created', field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name='application', name='updated', field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name='grant', name='created', field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name='grant', name='updated', field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name='refreshtoken', name='created', field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name='refreshtoken', name='updated', field=models.DateTimeField(auto_now=True), ), ] django-oauth-toolkit-1.0.0/oauth2_provider/migrations/__init__.py000066400000000000000000000000001311555211500251410ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/models.py000066400000000000000000000321361311555211500225300ustar00rootroot00000000000000from __future__ import unicode_literals from datetime import timedelta from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import models, transaction from django.urls import reverse from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from .compat import parse_qsl, urlparse from .generators import generate_client_id, generate_client_secret from .scopes import get_scopes_backend from .settings import oauth2_settings from .validators import validate_uris @python_2_unicode_compatible class AbstractApplication(models.Model): """ An Application instance represents a Client on the Authorization server. Usually an Application is created manually by client's developers after logging in on an Authorization Server. Fields: * :attr:`client_id` The client identifier issued to the client during the registration process as described in :rfc:`2.2` * :attr:`user` ref to a Django user * :attr:`redirect_uris` The list of allowed redirect uri. The string consists of valid URLs separated by space * :attr:`client_type` Client type as described in :rfc:`2.1` * :attr:`authorization_grant_type` Authorization flows available to the Application * :attr:`client_secret` Confidential secret issued to the client during the registration process as described in :rfc:`2.2` * :attr:`name` Friendly name for the Application """ CLIENT_CONFIDENTIAL = "confidential" CLIENT_PUBLIC = "public" CLIENT_TYPES = ( (CLIENT_CONFIDENTIAL, _("Confidential")), (CLIENT_PUBLIC, _("Public")), ) GRANT_AUTHORIZATION_CODE = "authorization-code" GRANT_IMPLICIT = "implicit" GRANT_PASSWORD = "password" GRANT_CLIENT_CREDENTIALS = "client-credentials" GRANT_TYPES = ( (GRANT_AUTHORIZATION_CODE, _("Authorization code")), (GRANT_IMPLICIT, _("Implicit")), (GRANT_PASSWORD, _("Resource owner password-based")), (GRANT_CLIENT_CREDENTIALS, _("Client credentials")), ) id = models.BigAutoField(primary_key=True) client_id = models.CharField( max_length=100, unique=True, default=generate_client_id, db_index=True ) user = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s", null=True, blank=True, on_delete=models.CASCADE ) help_text = _("Allowed URIs list, space separated") redirect_uris = models.TextField( blank=True, help_text=help_text, validators=[validate_uris] ) client_type = models.CharField(max_length=32, choices=CLIENT_TYPES) authorization_grant_type = models.CharField( max_length=32, choices=GRANT_TYPES ) client_secret = models.CharField( max_length=255, blank=True, default=generate_client_secret, db_index=True ) name = models.CharField(max_length=255, blank=True) skip_authorization = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) class Meta: abstract = True @property def default_redirect_uri(self): """ Returns the default redirect_uri extracting the first item from the :attr:`redirect_uris` string """ if self.redirect_uris: return self.redirect_uris.split().pop(0) assert False, ( "If you are using implicit, authorization_code" "or all-in-one grant_type, you must define " "redirect_uris field in your Application model" ) def redirect_uri_allowed(self, uri): """ Checks if given url is one of the items in :attr:`redirect_uris` string :param uri: Url to check """ for allowed_uri in self.redirect_uris.split(): parsed_allowed_uri = urlparse(allowed_uri) parsed_uri = urlparse(uri) if (parsed_allowed_uri.scheme == parsed_uri.scheme and parsed_allowed_uri.netloc == parsed_uri.netloc and parsed_allowed_uri.path == parsed_uri.path): aqs_set = set(parse_qsl(parsed_allowed_uri.query)) uqs_set = set(parse_qsl(parsed_uri.query)) if aqs_set.issubset(uqs_set): return True return False def clean(self): from django.core.exceptions import ValidationError if not self.redirect_uris \ and self.authorization_grant_type \ in (AbstractApplication.GRANT_AUTHORIZATION_CODE, AbstractApplication.GRANT_IMPLICIT): error = _("Redirect_uris could not be empty with {grant_type} grant_type") raise ValidationError(error.format(grant_type=self.authorization_grant_type)) def get_absolute_url(self): return reverse("oauth2_provider:detail", args=[str(self.id)]) def __str__(self): return self.name or self.client_id def allows_grant_type(self, *grant_types): return self.authorization_grant_type in grant_types def is_usable(self, request): """ Determines whether the application can be used. :param request: The HTTP request being processed. """ return True class Application(AbstractApplication): class Meta(AbstractApplication.Meta): swappable = "OAUTH2_PROVIDER_APPLICATION_MODEL" @python_2_unicode_compatible class AbstractGrant(models.Model): """ A Grant instance represents a token with a short lifetime that can be swapped for an access token, as described in :rfc:`4.1.2` Fields: * :attr:`user` The Django user who requested the grant * :attr:`code` The authorization code generated by the authorization server * :attr:`application` Application instance this grant was asked for * :attr:`expires` Expire time in seconds, defaults to :data:`settings.AUTHORIZATION_CODE_EXPIRE_SECONDS` * :attr:`redirect_uri` Self explained * :attr:`scope` Required scopes, optional """ id = models.BigAutoField(primary_key=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s" ) code = models.CharField(max_length=255, unique=True) # code comes from oauthlib application = models.ForeignKey( oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE ) expires = models.DateTimeField() redirect_uri = models.CharField(max_length=255) scope = models.TextField(blank=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) def is_expired(self): """ Check token expiration with timezone awareness """ if not self.expires: return True return timezone.now() >= self.expires def redirect_uri_allowed(self, uri): return uri == self.redirect_uri def __str__(self): return self.code class Meta: abstract = True class Grant(AbstractGrant): class Meta(AbstractGrant.Meta): swappable = "OAUTH2_PROVIDER_GRANT_MODEL" @python_2_unicode_compatible class AbstractAccessToken(models.Model): """ An AccessToken instance represents the actual access token to access user's resources, as in :rfc:`5`. Fields: * :attr:`user` The Django user representing resources' owner * :attr:`token` Access token * :attr:`application` Application instance * :attr:`expires` Date and time of token expiration, in DateTime format * :attr:`scope` Allowed scopes """ id = models.BigAutoField(primary_key=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True, related_name="%(app_label)s_%(class)s" ) token = models.CharField(max_length=255, unique=True, ) application = models.ForeignKey( oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, blank=True, null=True, ) expires = models.DateTimeField() scope = models.TextField(blank=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) def is_valid(self, scopes=None): """ Checks if the access token is valid. :param scopes: An iterable containing the scopes to check or None """ return not self.is_expired() and self.allow_scopes(scopes) def is_expired(self): """ Check token expiration with timezone awareness """ if not self.expires: return True return timezone.now() >= self.expires def allow_scopes(self, scopes): """ Check if the token allows the provided scopes :param scopes: An iterable containing the scopes to check """ if not scopes: return True provided_scopes = set(self.scope.split()) resource_scopes = set(scopes) return resource_scopes.issubset(provided_scopes) def revoke(self): """ Convenience method to uniform tokens' interface, for now simply remove this token from the database in order to revoke it. """ self.delete() @property def scopes(self): """ Returns a dictionary of allowed scope names (as keys) with their descriptions (as values) """ all_scopes = get_scopes_backend().get_all_scopes() token_scopes = self.scope.split() return {name: desc for name, desc in all_scopes.items() if name in token_scopes} def __str__(self): return self.token class Meta: abstract = True class AccessToken(AbstractAccessToken): class Meta(AbstractAccessToken.Meta): swappable = "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL" @python_2_unicode_compatible class AbstractRefreshToken(models.Model): """ A RefreshToken instance represents a token that can be swapped for a new access token when it expires. Fields: * :attr:`user` The Django user representing resources' owner * :attr:`token` Token value * :attr:`application` Application instance * :attr:`access_token` AccessToken instance this refresh token is bounded to """ id = models.BigAutoField(primary_key=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s" ) token = models.CharField(max_length=255, unique=True) application = models.ForeignKey( oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE) access_token = models.OneToOneField( oauth2_settings.ACCESS_TOKEN_MODEL, on_delete=models.CASCADE, related_name="refresh_token" ) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) def revoke(self): """ Delete this refresh token along with related access token """ access_token_model = get_access_token_model() access_token_model.objects.get(id=self.access_token.id).revoke() self.delete() def __str__(self): return self.token class Meta: abstract = True class RefreshToken(AbstractRefreshToken): class Meta(AbstractRefreshToken.Meta): swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL" def get_application_model(): """ Return the Application model that is active in this project. """ return apps.get_model(oauth2_settings.APPLICATION_MODEL) def get_grant_model(): """ Return the Grant model that is active in this project. """ return apps.get_model(oauth2_settings.GRANT_MODEL) def get_access_token_model(): """ Return the AccessToken model that is active in this project. """ return apps.get_model(oauth2_settings.ACCESS_TOKEN_MODEL) def get_refresh_token_model(): """ Return the RefreshToken model that is active in this project. """ return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL) def clear_expired(): now = timezone.now() refresh_expire_at = None access_token_model = get_access_token_model() refresh_token_model = get_refresh_token_model() grant_model = get_grant_model() REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS if REFRESH_TOKEN_EXPIRE_SECONDS: if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta): try: REFRESH_TOKEN_EXPIRE_SECONDS = timedelta(seconds=REFRESH_TOKEN_EXPIRE_SECONDS) except TypeError: e = "REFRESH_TOKEN_EXPIRE_SECONDS must be either a timedelta or seconds" raise ImproperlyConfigured(e) refresh_expire_at = now - REFRESH_TOKEN_EXPIRE_SECONDS with transaction.atomic(): if refresh_expire_at: refresh_token_model.objects.filter(access_token__expires__lt=refresh_expire_at).delete() access_token_model.objects.filter(refresh_token__isnull=True, expires__lt=now).delete() grant_model.objects.filter(expires__lt=now).delete() django-oauth-toolkit-1.0.0/oauth2_provider/oauth2_backends.py000066400000000000000000000163051311555211500243010ustar00rootroot00000000000000from __future__ import unicode_literals import json from oauthlib import oauth2 from oauthlib.common import quote, urlencode, urlencoded from .compat import urlparse, urlunparse from .exceptions import FatalClientError, OAuthToolkitError from .settings import oauth2_settings class OAuthLibCore(object): """ TODO: add docs """ def __init__(self, server=None): """ :params server: An instance of oauthlib.oauth2.Server class """ self.server = server or oauth2_settings.OAUTH2_SERVER_CLASS(oauth2_settings.OAUTH2_VALIDATOR_CLASS()) def _get_escaped_full_path(self, request): """ Django considers "safe" some characters that aren't so for oauthlib. We have to search for them and properly escape. """ parsed = list(urlparse(request.get_full_path())) unsafe = set(c for c in parsed[4]).difference(urlencoded) for c in unsafe: parsed[4] = parsed[4].replace(c, quote(c, safe=b"")) return urlunparse(parsed) def _get_extra_credentials(self, request): """ Produce extra credentials for token response. This dictionary will be merged with the response. See also: `oauthlib.oauth2.rfc6749.TokenEndpoint.create_token_response` :param request: The current django.http.HttpRequest object :return: dictionary of extra credentials or None (default) """ return None def _extract_params(self, request): """ Extract parameters from the Django request object. Such parameters will then be passed to OAuthLib to build its own Request object. The body should be encoded using OAuthLib urlencoded. """ uri = self._get_escaped_full_path(request) http_method = request.method headers = self.extract_headers(request) body = urlencode(self.extract_body(request)) return uri, http_method, body, headers def extract_headers(self, request): """ Extracts headers from the Django request object :param request: The current django.http.HttpRequest object :return: a dictionary with OAuthLib needed headers """ headers = request.META.copy() if "wsgi.input" in headers: del headers["wsgi.input"] if "wsgi.errors" in headers: del headers["wsgi.errors"] if "HTTP_AUTHORIZATION" in headers: headers["Authorization"] = headers["HTTP_AUTHORIZATION"] return headers def extract_body(self, request): """ Extracts the POST body from the Django request object :param request: The current django.http.HttpRequest object :return: provided POST parameters """ return request.POST.items() def validate_authorization_request(self, request): """ A wrapper method that calls validate_authorization_request on `server_class` instance. :param request: The current django.http.HttpRequest object """ try: uri, http_method, body, headers = self._extract_params(request) scopes, credentials = self.server.validate_authorization_request( uri, http_method=http_method, body=body, headers=headers) return scopes, credentials except oauth2.FatalClientError as error: raise FatalClientError(error=error) except oauth2.OAuth2Error as error: raise OAuthToolkitError(error=error) def create_authorization_response(self, request, scopes, credentials, allow): """ A wrapper method that calls create_authorization_response on `server_class` instance. :param request: The current django.http.HttpRequest object :param scopes: A list of provided scopes :param credentials: Authorization credentials dictionary containing `client_id`, `state`, `redirect_uri`, `response_type` :param allow: True if the user authorize the client, otherwise False """ try: if not allow: raise oauth2.AccessDeniedError() # add current user to credentials. this will be used by OAUTH2_VALIDATOR_CLASS credentials["user"] = request.user headers, body, status = self.server.create_authorization_response( uri=credentials["redirect_uri"], scopes=scopes, credentials=credentials) uri = headers.get("Location", None) return uri, headers, body, status except oauth2.FatalClientError as error: raise FatalClientError(error=error, redirect_uri=credentials["redirect_uri"]) except oauth2.OAuth2Error as error: raise OAuthToolkitError(error=error, redirect_uri=credentials["redirect_uri"]) def create_token_response(self, request): """ A wrapper method that calls create_token_response on `server_class` instance. :param request: The current django.http.HttpRequest object """ uri, http_method, body, headers = self._extract_params(request) extra_credentials = self._get_extra_credentials(request) headers, body, status = self.server.create_token_response(uri, http_method, body, headers, extra_credentials) uri = headers.get("Location", None) return uri, headers, body, status def create_revocation_response(self, request): """ A wrapper method that calls create_revocation_response on a `server_class` instance. :param request: The current django.http.HttpRequest object """ uri, http_method, body, headers = self._extract_params(request) headers, body, status = self.server.create_revocation_response( uri, http_method, body, headers) uri = headers.get("Location", None) return uri, headers, body, status def verify_request(self, request, scopes): """ A wrapper method that calls verify_request on `server_class` instance. :param request: The current django.http.HttpRequest object :param scopes: A list of scopes required to verify so that request is verified """ uri, http_method, body, headers = self._extract_params(request) valid, r = self.server.verify_request(uri, http_method, body, headers, scopes=scopes) return valid, r class JSONOAuthLibCore(OAuthLibCore): """ Extends the default OAuthLibCore to parse correctly application/json requests """ def extract_body(self, request): """ Extracts the JSON body from the Django request object :param request: The current django.http.HttpRequest object :return: provided POST parameters "urlencodable" """ try: body = json.loads(request.body.decode("utf-8")).items() except ValueError: body = "" return body def get_oauthlib_core(): """ Utility function that take a request and returns an instance of `oauth2_provider.backends.OAuthLibCore` """ validator = oauth2_settings.OAUTH2_VALIDATOR_CLASS() server = oauth2_settings.OAUTH2_SERVER_CLASS(validator) return oauth2_settings.OAUTH2_BACKEND_CLASS(server) django-oauth-toolkit-1.0.0/oauth2_provider/oauth2_validators.py000066400000000000000000000523121311555211500246750ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import binascii import logging from datetime import datetime, timedelta import requests from django.conf import settings from django.contrib.auth import authenticate, get_user_model from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.utils import timezone from django.utils.timezone import make_aware from oauthlib.oauth2 import RequestValidator from .compat import unquote_plus from .exceptions import FatalClientError from .models import ( AbstractApplication, get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model, ) from .scopes import get_scopes_backend from .settings import oauth2_settings log = logging.getLogger("oauth2_provider") GRANT_TYPE_MAPPING = { "authorization_code": (AbstractApplication.GRANT_AUTHORIZATION_CODE, ), "password": (AbstractApplication.GRANT_PASSWORD, ), "client_credentials": (AbstractApplication.GRANT_CLIENT_CREDENTIALS, ), "refresh_token": ( AbstractApplication.GRANT_AUTHORIZATION_CODE, AbstractApplication.GRANT_PASSWORD, AbstractApplication.GRANT_CLIENT_CREDENTIALS, ) } Application = get_application_model() AccessToken = get_access_token_model() Grant = get_grant_model() RefreshToken = get_refresh_token_model() UserModel = get_user_model() class OAuth2Validator(RequestValidator): def _extract_basic_auth(self, request): """ Return authentication string if request contains basic auth credentials, otherwise return None """ auth = request.headers.get("HTTP_AUTHORIZATION", None) if not auth: return None splitted = auth.split(" ", 1) if len(splitted) != 2: return None auth_type, auth_string = splitted if auth_type != "Basic": return None return auth_string def _authenticate_basic_auth(self, request): """ Authenticates with HTTP Basic Auth. Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with "application/x-www-form-urlencoded" encoding algorithm. """ auth_string = self._extract_basic_auth(request) if not auth_string: return False try: encoding = request.encoding or settings.DEFAULT_CHARSET or "utf-8" except AttributeError: encoding = "utf-8" try: b64_decoded = base64.b64decode(auth_string) except (TypeError, binascii.Error): log.debug("Failed basic auth: %r can't be decoded as base64", auth_string) return False try: auth_string_decoded = b64_decoded.decode(encoding) except UnicodeDecodeError: log.debug( "Failed basic auth: %r can't be decoded as unicode by %r", auth_string, encoding ) return False client_id, client_secret = map(unquote_plus, auth_string_decoded.split(":", 1)) if self._load_application(client_id, request) is None: log.debug("Failed basic auth: Application %s does not exist" % client_id) return False elif request.client.client_id != client_id: log.debug("Failed basic auth: wrong client id %s" % client_id) return False elif request.client.client_secret != client_secret: log.debug("Failed basic auth: wrong client secret %s" % client_secret) return False else: return True def _authenticate_request_body(self, request): """ Try to authenticate the client using client_id and client_secret parameters included in body. Remember that this method is NOT RECOMMENDED and SHOULD be limited to clients unable to directly utilize the HTTP Basic authentication scheme. See rfc:`2.3.1` for more details. """ # TODO: check if oauthlib has already unquoted client_id and client_secret try: client_id = request.client_id client_secret = request.client_secret except AttributeError: return False if self._load_application(client_id, request) is None: log.debug("Failed body auth: Application %s does not exists" % client_id) return False elif request.client.client_secret != client_secret: log.debug("Failed body auth: wrong client secret %s" % client_secret) return False else: return True def _load_application(self, client_id, request): """ If request.client was not set, load application instance for given client_id and store it in request.client """ # we want to be sure that request has the client attribute! assert hasattr(request, "client"), '"request" instance has no "client" attribute' try: request.client = request.client or Application.objects.get(client_id=client_id) # Check that the application can be used (defaults to always True) if not request.client.is_usable(request): log.debug("Failed body authentication: Application %r is disabled" % (client_id)) return None return request.client except Application.DoesNotExist: log.debug("Failed body authentication: Application %r does not exist" % (client_id)) return None def client_authentication_required(self, request, *args, **kwargs): """ Determine if the client has to be authenticated This method is called only for grant types that supports client authentication: * Authorization code grant * Resource owner password grant * Refresh token grant If the request contains authorization headers, always authenticate the client no matter the grant type. If the request does not contain authorization headers, proceed with authentication only if the client is of type `Confidential`. If something goes wrong, call oauthlib implementation of the method. """ if self._extract_basic_auth(request): return True try: if request.client_id and request.client_secret: return True except AttributeError: log.debug("Client ID or client secret not provided...") pass self._load_application(request.client_id, request) if request.client: return request.client.client_type == AbstractApplication.CLIENT_CONFIDENTIAL return super(OAuth2Validator, self).client_authentication_required(request, *args, **kwargs) def authenticate_client(self, request, *args, **kwargs): """ Check if client exists and is authenticating itself as in rfc:`3.2.1` First we try to authenticate with HTTP Basic Auth, and that is the PREFERRED authentication method. Whether this fails we support including the client credentials in the request-body, but this method is NOT RECOMMENDED and SHOULD be limited to clients unable to directly utilize the HTTP Basic authentication scheme. See rfc:`2.3.1` for more details """ authenticated = self._authenticate_basic_auth(request) if not authenticated: authenticated = self._authenticate_request_body(request) return authenticated def authenticate_client_id(self, client_id, request, *args, **kwargs): """ If we are here, the client did not authenticate itself as in rfc:`3.2.1` and we can proceed only if the client exists and is not of type "Confidential". """ if self._load_application(client_id, request) is not None: log.debug("Application %r has type %r" % (client_id, request.client.client_type)) return request.client.client_type != AbstractApplication.CLIENT_CONFIDENTIAL return False def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): """ Ensure the redirect_uri is listed in the Application instance redirect_uris field """ grant = Grant.objects.get(code=code, application=client) return grant.redirect_uri_allowed(redirect_uri) def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """ Remove the temporary grant used to swap the authorization token """ grant = Grant.objects.get(code=code, application=request.client) grant.delete() def validate_client_id(self, client_id, request, *args, **kwargs): """ Ensure an Application exists with given client_id. If it exists, it's assigned to request.client. """ return self._load_application(client_id, request) is not None def get_default_redirect_uri(self, client_id, request, *args, **kwargs): return request.client.default_redirect_uri def _get_token_from_authentication_server(self, token, introspection_url, introspection_token): bearer = "Bearer {}".format(introspection_token) try: response = requests.post( introspection_url, data={"token": token}, headers={"Authorization": bearer} ) except requests.exceptions.RequestException: log.exception("Introspection: Failed POST to %r in token lookup", introspection_url) return None try: content = response.json() except ValueError: log.exception("Introspection: Failed to parse response as json") return None if "active" in content and content["active"] is True: if "username" in content: user, _created = UserModel.objects.get_or_create( **{UserModel.USERNAME_FIELD: content["username"]} ) else: user = None max_caching_time = datetime.now() + timedelta( seconds=oauth2_settings.RESOURCE_SERVER_TOKEN_CACHING_SECONDS ) if "exp" in content: expires = datetime.utcfromtimestamp(content["exp"]) if expires > max_caching_time: expires = max_caching_time else: expires = max_caching_time scope = content.get("scope", "") expires = make_aware(expires) try: access_token = AccessToken.objects.select_related("application", "user").get(token=token) except AccessToken.DoesNotExist: access_token = AccessToken.objects.create( token=token, user=user, application=None, scope=scope, expires=expires ) else: access_token.expires = expires access_token.scope = scope access_token.save() return access_token def validate_bearer_token(self, token, scopes, request): """ When users try to access resources, check that provided token is valid """ if not token: return False introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN try: access_token = AccessToken.objects.select_related("application", "user").get(token=token) # if there is a token but invalid then look up the token if introspection_url and introspection_token: if not access_token.is_valid(scopes): access_token = self._get_token_from_authentication_server( token, introspection_url, introspection_token ) if access_token and access_token.is_valid(scopes): request.client = access_token.application request.user = access_token.user request.scopes = scopes # this is needed by django rest framework request.access_token = access_token return True return False except AccessToken.DoesNotExist: # there is no initial token, look up the token if introspection_url and introspection_token: access_token = self._get_token_from_authentication_server( token, introspection_url, introspection_token ) if access_token and access_token.is_valid(scopes): request.client = access_token.application request.user = access_token.user request.scopes = scopes # this is needed by django rest framework request.access_token = access_token return True return False def validate_code(self, client_id, code, client, request, *args, **kwargs): try: grant = Grant.objects.get(code=code, application=client) if not grant.is_expired(): request.scopes = grant.scope.split(" ") request.user = grant.user return True return False except Grant.DoesNotExist: return False def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): """ Validate both grant_type is a valid string and grant_type is allowed for current workflow """ assert(grant_type in GRANT_TYPE_MAPPING) # mapping misconfiguration return request.client.allows_grant_type(*GRANT_TYPE_MAPPING[grant_type]) def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): """ We currently do not support the Authorization Endpoint Response Types registry as in rfc:`8.4`, so validate the response_type only if it matches "code" or "token" """ if response_type == "code": return client.allows_grant_type(AbstractApplication.GRANT_AUTHORIZATION_CODE) elif response_type == "token": return client.allows_grant_type(AbstractApplication.GRANT_IMPLICIT) else: return False def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): """ Ensure required scopes are permitted (as specified in the settings file) """ available_scopes = get_scopes_backend().get_available_scopes(application=client, request=request) return set(scopes).issubset(set(available_scopes)) def get_default_scopes(self, client_id, request, *args, **kwargs): default_scopes = get_scopes_backend().get_default_scopes(application=request.client, request=request) return default_scopes def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): return request.client.redirect_uri_allowed(redirect_uri) def save_authorization_code(self, client_id, code, request, *args, **kwargs): expires = timezone.now() + timedelta( seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS) g = Grant(application=request.client, user=request.user, code=code["code"], expires=expires, redirect_uri=request.redirect_uri, scope=" ".join(request.scopes)) g.save() def rotate_refresh_token(self, request): """ Checks if rotate refresh token is enabled """ return oauth2_settings.ROTATE_REFRESH_TOKEN @transaction.atomic def save_bearer_token(self, token, request, *args, **kwargs): """ Save access and refresh token, If refresh token is issued, remove or reuse old refresh token as in rfc:`6` @see: https://tools.ietf.org/html/draft-ietf-oauth-v2-31#page-43 """ if "scope" not in token: raise FatalClientError("Failed to renew access token: missing scope") expires = timezone.now() + timedelta(seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) if request.grant_type == "client_credentials": request.user = None # This comes from OAuthLib: # https://github.com/idan/oauthlib/blob/1.0.3/oauthlib/oauth2/rfc6749/tokens.py#L267 # Its value is either a new random code; or if we are reusing # refresh tokens, then it is the same value that the request passed in # (stored in `request.refresh_token`) refresh_token_code = token.get("refresh_token", None) if refresh_token_code: # an instance of `RefreshToken` that matches the old refresh code. # Set on the request in `validate_refresh_token` refresh_token_instance = getattr(request, "refresh_token_instance", None) # If we are to reuse tokens, and we can: do so if not self.rotate_refresh_token(request) and \ isinstance(refresh_token_instance, RefreshToken) and \ refresh_token_instance.access_token: access_token = AccessToken.objects.select_for_update().get( pk=refresh_token_instance.access_token.pk ) access_token.user = request.user access_token.scope = token["scope"] access_token.expires = expires access_token.token = token["access_token"] access_token.application = request.client access_token.save() # else create fresh with access & refresh tokens else: # revoke existing tokens if possible if isinstance(refresh_token_instance, RefreshToken): try: refresh_token_instance.revoke() except (AccessToken.DoesNotExist, RefreshToken.DoesNotExist): pass else: setattr(request, "refresh_token_instance", None) access_token = self._create_access_token(expires, request, token) refresh_token = RefreshToken( user=request.user, token=refresh_token_code, application=request.client, access_token=access_token ) refresh_token.save() # No refresh token should be created, just access token else: self._create_access_token(expires, request, token) # TODO: check out a more reliable way to communicate expire time to oauthlib token["expires_in"] = oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS def _create_access_token(self, expires, request, token): access_token = AccessToken( user=request.user, scope=token["scope"], expires=expires, token=token["access_token"], application=request.client ) access_token.save() return access_token def revoke_token(self, token, token_type_hint, request, *args, **kwargs): """ Revoke an access or refresh token. :param token: The token string. :param token_type_hint: access_token or refresh_token. :param request: The HTTP Request (oauthlib.common.Request) """ if token_type_hint not in ["access_token", "refresh_token"]: token_type_hint = None token_types = { "access_token": AccessToken, "refresh_token": RefreshToken, } token_type = token_types.get(token_type_hint, AccessToken) try: token_type.objects.get(token=token).revoke() except ObjectDoesNotExist: for other_type in [_t for _t in token_types.values() if _t != token_type]: # slightly inefficient on Python2, but the queryset contains only one instance list(map(lambda t: t.revoke(), other_type.objects.filter(token=token))) def validate_user(self, username, password, client, request, *args, **kwargs): """ Check username and password correspond to a valid and active User """ u = authenticate(username=username, password=password) if u is not None and u.is_active: request.user = u return True return False def get_original_scopes(self, refresh_token, request, *args, **kwargs): # Avoid second query for RefreshToken since this method is invoked *after* # validate_refresh_token. rt = request.refresh_token_instance return rt.access_token.scope def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): """ Check refresh_token exists and refers to the right client. Also attach User instance to the request object """ try: rt = RefreshToken.objects.get(token=refresh_token) request.user = rt.user request.refresh_token = rt.token # Temporary store RefreshToken instance to be reused by get_original_scopes. request.refresh_token_instance = rt return rt.application == client except RefreshToken.DoesNotExist: return False django-oauth-toolkit-1.0.0/oauth2_provider/scopes.py000066400000000000000000000031701311555211500225350ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals from .settings import oauth2_settings class BaseScopes(object): def get_all_scopes(self): """ Return a dict-like object with all the scopes available in the system. The key should be the scope name and the value should be the description. ex: {"read": "A read scope", "write": "A write scope"} """ raise NotImplementedError("") def get_available_scopes(self, application=None, request=None, *args, **kwargs): """ Return a list of scopes available for the current application/request. TODO: add info on where and why this method is called. ex: ["read", "write"] """ raise NotImplementedError("") def get_default_scopes(self, application=None, request=None, *args, **kwargs): """ Return a list of the default scopes for the current application/request. This MUST be a subset of the scopes returned by `get_available_scopes`. TODO: add info on where and why this method is called. ex: ["read"] """ raise NotImplementedError("") class SettingsScopes(BaseScopes): def get_all_scopes(self): return oauth2_settings.SCOPES def get_available_scopes(self, application=None, request=None, *args, **kwargs): return oauth2_settings._SCOPES def get_default_scopes(self, application=None, request=None, *args, **kwargs): return oauth2_settings._DEFAULT_SCOPES def get_scopes_backend(): scopes_class = oauth2_settings.SCOPES_BACKEND_CLASS return scopes_class() django-oauth-toolkit-1.0.0/oauth2_provider/settings.py000066400000000000000000000141221311555211500231000ustar00rootroot00000000000000""" This module is largely inspired by django-rest-framework settings. Settings for the OAuth2 Provider are all namespaced in the OAUTH2_PROVIDER setting. For example your project's `settings.py` file might look like this: OAUTH2_PROVIDER = { "CLIENT_ID_GENERATOR_CLASS": "oauth2_provider.generators.ClientIdGenerator", "CLIENT_SECRET_GENERATOR_CLASS": "oauth2_provider.generators.ClientSecretGenerator", } This module provides the `oauth2_settings` object, that is used to access OAuth2 Provider settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals import importlib from django.conf import settings from django.core.exceptions import ImproperlyConfigured USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None) APPLICATION_MODEL = getattr(settings, "OAUTH2_PROVIDER_APPLICATION_MODEL", "oauth2_provider.Application") ACCESS_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL", "oauth2_provider.AccessToken") GRANT_MODEL = getattr(settings, "OAUTH2_PROVIDER_GRANT_MODEL", "oauth2_provider.Grant") REFRESH_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_REFRESH_MODEL", "oauth2_provider.RefreshToken") DEFAULTS = { "CLIENT_ID_GENERATOR_CLASS": "oauth2_provider.generators.ClientIdGenerator", "CLIENT_SECRET_GENERATOR_CLASS": "oauth2_provider.generators.ClientSecretGenerator", "CLIENT_SECRET_GENERATOR_LENGTH": 128, "OAUTH2_SERVER_CLASS": "oauthlib.oauth2.Server", "OAUTH2_VALIDATOR_CLASS": "oauth2_provider.oauth2_validators.OAuth2Validator", "OAUTH2_BACKEND_CLASS": "oauth2_provider.oauth2_backends.OAuthLibCore", "SCOPES": {"read": "Reading scope", "write": "Writing scope"}, "DEFAULT_SCOPES": ["__all__"], "SCOPES_BACKEND_CLASS": "oauth2_provider.scopes.SettingsScopes", "READ_SCOPE": "read", "WRITE_SCOPE": "write", "AUTHORIZATION_CODE_EXPIRE_SECONDS": 60, "ACCESS_TOKEN_EXPIRE_SECONDS": 36000, "REFRESH_TOKEN_EXPIRE_SECONDS": None, "ROTATE_REFRESH_TOKEN": True, "APPLICATION_MODEL": APPLICATION_MODEL, "ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL, "GRANT_MODEL": GRANT_MODEL, "REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL, "REQUEST_APPROVAL_PROMPT": "force", "ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"], # Special settings that will be evaluated at runtime "_SCOPES": [], "_DEFAULT_SCOPES": [], # Resource Server with Token Introspection "RESOURCE_SERVER_INTROSPECTION_URL": None, "RESOURCE_SERVER_AUTH_TOKEN": None, "RESOURCE_SERVER_TOKEN_CACHING_SECONDS": 36000, } # List of settings that cannot be empty MANDATORY = ( "CLIENT_ID_GENERATOR_CLASS", "CLIENT_SECRET_GENERATOR_CLASS", "OAUTH2_SERVER_CLASS", "OAUTH2_VALIDATOR_CLASS", "OAUTH2_BACKEND_CLASS", "SCOPES", "ALLOWED_REDIRECT_URI_SCHEMES", ) # List of settings that may be in string import notation. IMPORT_STRINGS = ( "CLIENT_ID_GENERATOR_CLASS", "CLIENT_SECRET_GENERATOR_CLASS", "OAUTH2_SERVER_CLASS", "OAUTH2_VALIDATOR_CLASS", "OAUTH2_BACKEND_CLASS", "SCOPES_BACKEND_CLASS", ) def perform_import(val, setting_name): """ If the given setting is a string import notation, then perform the necessary import or imports. """ if isinstance(val, (list, tuple)): return [import_from_string(item, setting_name) for item in val] elif "." in val: return import_from_string(val, setting_name) else: raise ImproperlyConfigured("Bad value for %r: %r" % (setting_name, val)) def import_from_string(val, setting_name): """ Attempt to import a class from a string representation. """ try: parts = val.split(".") module_path, class_name = ".".join(parts[:-1]), parts[-1] module = importlib.import_module(module_path) return getattr(module, class_name) except ImportError as e: msg = "Could not import %r for setting %r. %s: %s." % (val, setting_name, e.__class__.__name__, e) raise ImportError(msg) class OAuth2ProviderSettings(object): """ A settings object, that allows OAuth2 Provider settings to be accessed as properties. Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. """ def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None): self.user_settings = user_settings or {} self.defaults = defaults or {} self.import_strings = import_strings or () self.mandatory = mandatory or () def __getattr__(self, attr): if attr not in self.defaults.keys(): raise AttributeError("Invalid OAuth2Provider setting: %r" % (attr)) try: # Check if present in user settings val = self.user_settings[attr] except KeyError: # Fall back to defaults val = self.defaults[attr] # Coerce import strings into classes if val and attr in self.import_strings: val = perform_import(val, attr) # Overriding special settings if attr == "_SCOPES": val = list(self.SCOPES.keys()) if attr == "_DEFAULT_SCOPES": if "__all__" in self.DEFAULT_SCOPES: # If DEFAULT_SCOPES is set to ["__all__"] the whole set of scopes is returned val = list(self._SCOPES) else: # Otherwise we return a subset (that can be void) of SCOPES val = [] for scope in self.DEFAULT_SCOPES: if scope in self._SCOPES: val.append(scope) else: raise ImproperlyConfigured("Defined DEFAULT_SCOPES not present in SCOPES") self.validate_setting(attr, val) # Cache the result setattr(self, attr, val) return val def validate_setting(self, attr, val): if not val and attr in self.mandatory: raise AttributeError("OAuth2Provider setting: %r is mandatory" % (attr)) oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY) django-oauth-toolkit-1.0.0/oauth2_provider/templates/000077500000000000000000000000001311555211500226645ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/000077500000000000000000000000001311555211500260005ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html000066400000000000000000000013561311555211500342350ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}

{% trans "Are you sure to delete the application" %} {{ application.name }}?

{% csrf_token %}
{% endblock content %} django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/application_detail.html000066400000000000000000000030231311555211500325110ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}

{{ application.name }}

  • {% trans "Client id" %}

  • {% trans "Client secret" %}

  • {% trans "Client type" %}

    {{ application.client_type }}

  • {% trans "Authorization Grant Type" %}

    {{ application.authorization_grant_type }}

  • {% trans "Redirect Uris" %}

{% endblock content %} django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/application_form.html000066400000000000000000000034271311555211500322220ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}

{% block app-form-title %} {% trans "Edit application" %} {{ application.name }} {% endblock app-form-title %}

{% csrf_token %} {% for field in form %}
{{ field }} {% for error in field.errors %} {{ error }} {% endfor %}
{% endfor %}
{% for error in form.non_field_errors %} {{ error }} {% endfor %}
{% endblock %} django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/application_list.html000066400000000000000000000014341311555211500322260ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}

{% trans "Your applications" %}

{% if applications %} New Application {% else %}

{% trans "No applications defined" %}. {% trans "Click here" %} {% trans "if you want to register a new one" %}

{% endif %}
{% endblock content %} application_registration_form.html000066400000000000000000000005571311555211500347360ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider{% extends "oauth2_provider/application_form.html" %} {% load i18n %} {% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %} {% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %} {% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %} django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/authorize.html000066400000000000000000000025171311555211500307050ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}
{% if not error %}

{% trans "Authorize" %} {{ application.name }}?

{% csrf_token %} {% for field in form %} {% if field.is_hidden %} {{ field }} {% endif %} {% endfor %}

{% trans "Application requires following permissions" %}

    {% for scope in scopes_descriptions %}
  • {{ scope }}
  • {% endfor %}
{{ form.errors }} {{ form.non_field_errors }}
{% else %}

Error: {{ error.error }}

{{ error.description }}

{% endif %}
{% endblock %}django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html000066400000000000000000000004431311555211500334230ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}
{% csrf_token %}

{% trans "Are you sure you want to delete this token?" %}

{% endblock %} django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/authorized-tokens.html000066400000000000000000000014061311555211500323460ustar00rootroot00000000000000{% extends "oauth2_provider/base.html" %} {% load i18n %} {% block content %}

{% trans "Tokens" %}

    {% for authorized_token in authorized_tokens %}
  • {{ authorized_token.application }} (revoke)
    • {% for scope_name, scope_description in authorized_token.scopes.items %}
    • {{ scope_name }}: {{ scope_description }}
    • {% endfor %}
    {% empty %}
  • {% trans "There are no authorized tokens yet." %}
  • {% endfor %}
{% endblock %} django-oauth-toolkit-1.0.0/oauth2_provider/templates/oauth2_provider/base.html000066400000000000000000000024161311555211500276030ustar00rootroot00000000000000 {% block title %}{% endblock title %} {% block css %} {% endblock css %}
{% block content %} {% endblock content %}
django-oauth-toolkit-1.0.0/oauth2_provider/urls.py000066400000000000000000000024531311555211500222310ustar00rootroot00000000000000from __future__ import absolute_import from django.conf.urls import url from . import views app_name = "oauth2_provider" base_urlpatterns = [ url(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"), url(r"^token/$", views.TokenView.as_view(), name="token"), url(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"), url(r"^introspect/$", views.IntrospectTokenView.as_view(), name="introspect"), ] management_urlpatterns = [ # Application management views url(r"^applications/$", views.ApplicationList.as_view(), name="list"), url(r"^applications/register/$", views.ApplicationRegistration.as_view(), name="register"), url(r"^applications/(?P[\w-]+)/$", views.ApplicationDetail.as_view(), name="detail"), url(r"^applications/(?P[\w-]+)/delete/$", views.ApplicationDelete.as_view(), name="delete"), url(r"^applications/(?P[\w-]+)/update/$", views.ApplicationUpdate.as_view(), name="update"), # Token management views url(r"^authorized_tokens/$", views.AuthorizedTokensListView.as_view(), name="authorized-token-list"), url(r"^authorized_tokens/(?P[\w-]+)/delete/$", views.AuthorizedTokenDeleteView.as_view(), name="authorized-token-delete"), ] urlpatterns = base_urlpatterns + management_urlpatterns django-oauth-toolkit-1.0.0/oauth2_provider/validators.py000066400000000000000000000046731311555211500234220ustar00rootroot00000000000000from __future__ import unicode_literals import re from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from .compat import urlsplit, urlunsplit from .settings import oauth2_settings class URIValidator(RegexValidator): regex = re.compile( r"^(?:[a-z][a-z0-9\.\-\+]*)://" # scheme... r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain... r"(?!-)[A-Z\d-]{1,63}(? ACE except UnicodeError: # invalid domain part raise e url = urlunsplit((scheme, netloc, path, query, fragment)) super(URIValidator, self).__call__(url) else: raise else: url = value class RedirectURIValidator(URIValidator): def __init__(self, allowed_schemes): self.allowed_schemes = allowed_schemes def __call__(self, value): super(RedirectURIValidator, self).__call__(value) value = force_text(value) if len(value.split("#")) > 1: raise ValidationError("Redirect URIs must not contain fragments") scheme, netloc, path, query, fragment = urlsplit(value) if scheme.lower() not in self.allowed_schemes: raise ValidationError("Redirect URI scheme is not allowed.") def validate_uris(value): """ This validator ensures that `value` contains valid blank-separated URIs" """ v = RedirectURIValidator(oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES) uris = value.split() if not uris: raise ValidationError("Redirect URI cannot be empty") for uri in uris: v(uri) django-oauth-toolkit-1.0.0/oauth2_provider/views/000077500000000000000000000000001311555211500220235ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/oauth2_provider/views/__init__.py000066400000000000000000000006501311555211500241350ustar00rootroot00000000000000# flake8: noqa from .base import AuthorizationView, TokenView, RevokeTokenView from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \ ApplicationDelete, ApplicationUpdate from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView from .token import AuthorizedTokensListView, AuthorizedTokenDeleteView from .introspect import IntrospectTokenView django-oauth-toolkit-1.0.0/oauth2_provider/views/application.py000066400000000000000000000052451311555211500247060ustar00rootroot00000000000000from django.contrib.auth.mixins import LoginRequiredMixin from django.forms.models import modelform_factory from django.urls import reverse_lazy from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView from ..models import get_application_model class ApplicationOwnerIsUserMixin(LoginRequiredMixin): """ This mixin is used to provide an Application queryset filtered by the current request.user. """ fields = "__all__" def get_queryset(self): return get_application_model().objects.filter(user=self.request.user) class ApplicationRegistration(LoginRequiredMixin, CreateView): """ View used to register a new Application for the request.user """ template_name = "oauth2_provider/application_registration_form.html" def get_form_class(self): """ Returns the form class for the application model """ return modelform_factory( get_application_model(), fields=( "name", "client_id", "client_secret", "client_type", "authorization_grant_type", "redirect_uris" ) ) def form_valid(self, form): form.instance.user = self.request.user return super(ApplicationRegistration, self).form_valid(form) class ApplicationDetail(ApplicationOwnerIsUserMixin, DetailView): """ Detail view for an application instance owned by the request.user """ context_object_name = "application" template_name = "oauth2_provider/application_detail.html" class ApplicationList(ApplicationOwnerIsUserMixin, ListView): """ List view for all the applications owned by the request.user """ context_object_name = "applications" template_name = "oauth2_provider/application_list.html" class ApplicationDelete(ApplicationOwnerIsUserMixin, DeleteView): """ View used to delete an application owned by the request.user """ context_object_name = "application" success_url = reverse_lazy("oauth2_provider:list") template_name = "oauth2_provider/application_confirm_delete.html" class ApplicationUpdate(ApplicationOwnerIsUserMixin, UpdateView): """ View used to update an application owned by the request.user """ context_object_name = "application" template_name = "oauth2_provider/application_form.html" def get_form_class(self): """ Returns the form class for the application model """ return modelform_factory( get_application_model(), fields=( "name", "client_id", "client_secret", "client_type", "authorization_grant_type", "redirect_uris" ) ) django-oauth-toolkit-1.0.0/oauth2_provider/views/base.py000066400000000000000000000212661311555211500233160ustar00rootroot00000000000000import logging from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponse from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import FormView, View from .mixins import OAuthLibMixin from ..exceptions import OAuthToolkitError from ..forms import AllowForm from ..http import HttpResponseUriRedirect from ..models import get_access_token_model, get_application_model from ..scopes import get_scopes_backend from ..settings import oauth2_settings log = logging.getLogger("oauth2_provider") class BaseAuthorizationView(LoginRequiredMixin, OAuthLibMixin, View): """ Implements a generic endpoint to handle *Authorization Requests* as in :rfc:`4.1.1`. The view does not implement any strategy to determine *authorize/do not authorize* logic. The endpoint is used in the following flows: * Authorization code * Implicit grant """ def dispatch(self, request, *args, **kwargs): self.oauth2_data = {} return super(BaseAuthorizationView, self).dispatch(request, *args, **kwargs) def error_response(self, error, **kwargs): """ Handle errors either by redirecting to redirect_uri with a json in the body containing error details or providing an error response """ redirect, error_response = super(BaseAuthorizationView, self).error_response(error, **kwargs) if redirect: return HttpResponseUriRedirect(error_response["url"]) status = error_response["error"].status_code return self.render_to_response(error_response, status=status) class AuthorizationView(BaseAuthorizationView, FormView): """ Implements an endpoint to handle *Authorization Requests* as in :rfc:`4.1.1` and prompting the user with a form to determine if she authorizes the client application to access her data. This endpoint is reached two times during the authorization process: * first receive a ``GET`` request from user asking authorization for a certain client application, a form is served possibly showing some useful info and prompting for *authorize/do not authorize*. * then receive a ``POST`` request possibly after user authorized the access Some informations contained in the ``GET`` request and needed to create a Grant token during the ``POST`` request would be lost between the two steps above, so they are temporarily stored in hidden fields on the form. A possible alternative could be keeping such informations in the session. The endpoint is used in the following flows: * Authorization code * Implicit grant """ template_name = "oauth2_provider/authorize.html" form_class = AllowForm server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS skip_authorization_completely = False def get_initial(self): # TODO: move this scopes conversion from and to string into a utils function scopes = self.oauth2_data.get("scope", self.oauth2_data.get("scopes", [])) initial_data = { "redirect_uri": self.oauth2_data.get("redirect_uri", None), "scope": " ".join(scopes), "client_id": self.oauth2_data.get("client_id", None), "state": self.oauth2_data.get("state", None), "response_type": self.oauth2_data.get("response_type", None), } return initial_data def form_valid(self, form): try: credentials = { "client_id": form.cleaned_data.get("client_id"), "redirect_uri": form.cleaned_data.get("redirect_uri"), "response_type": form.cleaned_data.get("response_type", None), "state": form.cleaned_data.get("state", None), } scopes = form.cleaned_data.get("scope") allow = form.cleaned_data.get("allow") uri, headers, body, status = self.create_authorization_response( request=self.request, scopes=scopes, credentials=credentials, allow=allow) self.success_url = uri log.debug("Success url for the request: {0}".format(self.success_url)) return HttpResponseUriRedirect(self.success_url) except OAuthToolkitError as error: return self.error_response(error) def get(self, request, *args, **kwargs): try: scopes, credentials = self.validate_authorization_request(request) all_scopes = get_scopes_backend().get_all_scopes() kwargs["scopes_descriptions"] = [all_scopes[scope] for scope in scopes] kwargs["scopes"] = scopes # at this point we know an Application instance with such client_id exists in the database # TODO: Cache this! application = get_application_model().objects.get(client_id=credentials["client_id"]) kwargs["application"] = application kwargs["client_id"] = credentials["client_id"] kwargs["redirect_uri"] = credentials["redirect_uri"] kwargs["response_type"] = credentials["response_type"] kwargs["state"] = credentials["state"] self.oauth2_data = kwargs # following two loc are here only because of https://code.djangoproject.com/ticket/17795 form = self.get_form(self.get_form_class()) kwargs["form"] = form # Check to see if the user has already granted access and return # a successful response depending on "approval_prompt" url parameter require_approval = request.GET.get("approval_prompt", oauth2_settings.REQUEST_APPROVAL_PROMPT) # If skip_authorization field is True, skip the authorization screen even # if this is the first use of the application and there was no previous authorization. # This is useful for in-house applications-> assume an in-house applications # are already approved. if application.skip_authorization: uri, headers, body, status = self.create_authorization_response( request=self.request, scopes=" ".join(scopes), credentials=credentials, allow=True) return HttpResponseUriRedirect(uri) elif require_approval == "auto": tokens = get_access_token_model().objects.filter( user=request.user, application=kwargs["application"], expires__gt=timezone.now() ).all() # check past authorizations regarded the same scopes as the current one for token in tokens: if token.allow_scopes(scopes): uri, headers, body, status = self.create_authorization_response( request=self.request, scopes=" ".join(scopes), credentials=credentials, allow=True) return HttpResponseUriRedirect(uri) return self.render_to_response(self.get_context_data(**kwargs)) except OAuthToolkitError as error: return self.error_response(error) @method_decorator(csrf_exempt, name="dispatch") class TokenView(OAuthLibMixin, View): """ Implements an endpoint to provide access tokens The endpoint is used in the following flows: * Authorization code * Password * Client credentials """ server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS @method_decorator(sensitive_post_parameters("password")) def post(self, request, *args, **kwargs): url, headers, body, status = self.create_token_response(request) response = HttpResponse(content=body, status=status) for k, v in headers.items(): response[k] = v return response @method_decorator(csrf_exempt, name="dispatch") class RevokeTokenView(OAuthLibMixin, View): """ Implements an endpoint to revoke access or refresh tokens """ server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS def post(self, request, *args, **kwargs): url, headers, body, status = self.create_revocation_response(request) response = HttpResponse(content=body or "", status=status) for k, v in headers.items(): response[k] = v return response django-oauth-toolkit-1.0.0/oauth2_provider/views/generic.py000066400000000000000000000020131311555211500240050ustar00rootroot00000000000000from django.views.generic import View from .mixins import ProtectedResourceMixin, ReadWriteScopedResourceMixin, ScopedResourceMixin from ..settings import oauth2_settings class ProtectedResourceView(ProtectedResourceMixin, View): """ Generic view protecting resources by providing OAuth2 authentication out of the box """ server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS class ScopedProtectedResourceView(ScopedResourceMixin, ProtectedResourceView): """ Generic view protecting resources by providing OAuth2 authentication and Scopes handling out of the box """ pass class ReadWriteScopedResourceView(ReadWriteScopedResourceMixin, ProtectedResourceView): """ Generic view protecting resources with OAuth2 authentication and read/write scopes. GET, HEAD, OPTIONS http methods require "read" scope. Otherwise "write" scope is required. """ pass django-oauth-toolkit-1.0.0/oauth2_provider/views/introspect.py000066400000000000000000000047551311555211500246020ustar00rootroot00000000000000from __future__ import unicode_literals import calendar import json from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from oauth2_provider.models import get_access_token_model from oauth2_provider.views import ReadWriteScopedResourceView @method_decorator(csrf_exempt, name="dispatch") class IntrospectTokenView(ReadWriteScopedResourceView): """ Implements an endpoint for token introspection based on RFC 7662 https://tools.ietf.org/html/rfc7662 To access this view the request must pass a OAuth2 Bearer Token which is allowed to access the scope `introspection`. """ required_scopes = ["introspection"] @staticmethod def get_token_response(token_value=None): try: token = get_access_token_model().objects.get(token=token_value) except ObjectDoesNotExist: return HttpResponse( content=json.dumps({"active": False}), status=401, content_type="application/json" ) else: if token.is_valid(): data = { "active": True, "scope": token.scope, "exp": int(calendar.timegm(token.expires.timetuple())), } if token.application: data["client_id"] = token.application.client_id if token.user: data["username"] = token.user.get_username() return HttpResponse(content=json.dumps(data), status=200, content_type="application/json") else: return HttpResponse(content=json.dumps({ "active": False, }), status=200, content_type="application/json") def get(self, request, *args, **kwargs): """ Get the token from the URL parameters. URL: https://example.com/introspect?token=mF_9.B5f-4.1JqM :param request: :param args: :param kwargs: :return: """ return self.get_token_response(request.GET.get("token", None)) def post(self, request, *args, **kwargs): """ Get the token from the body form parameters. Body: token=mF_9.B5f-4.1JqM :param request: :param args: :param kwargs: :return: """ return self.get_token_response(request.POST.get("token", None)) django-oauth-toolkit-1.0.0/oauth2_provider/views/mixins.py000066400000000000000000000210021311555211500236770ustar00rootroot00000000000000from __future__ import unicode_literals import logging from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseForbidden from ..exceptions import FatalClientError from ..scopes import get_scopes_backend from ..settings import oauth2_settings log = logging.getLogger("oauth2_provider") SAFE_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"] class OAuthLibMixin(object): """ This mixin decouples Django OAuth Toolkit from OAuthLib. Users can configure the Server, Validator and OAuthlibCore classes used by this mixin by setting the following class variables: * server_class * validator_class * oauthlib_backend_class """ server_class = None validator_class = None oauthlib_backend_class = None @classmethod def get_server_class(cls): """ Return the OAuthlib server class to use """ if cls.server_class is None: raise ImproperlyConfigured( "OAuthLibMixin requires either a definition of 'server_class'" " or an implementation of 'get_server_class()'") else: return cls.server_class @classmethod def get_validator_class(cls): """ Return the RequestValidator implementation class to use """ if cls.validator_class is None: raise ImproperlyConfigured( "OAuthLibMixin requires either a definition of 'validator_class'" " or an implementation of 'get_validator_class()'") else: return cls.validator_class @classmethod def get_oauthlib_backend_class(cls): """ Return the OAuthLibCore implementation class to use """ if cls.oauthlib_backend_class is None: raise ImproperlyConfigured( "OAuthLibMixin requires either a definition of 'oauthlib_backend_class'" " or an implementation of 'get_oauthlib_backend_class()'") else: return cls.oauthlib_backend_class @classmethod def get_server(cls): """ Return an instance of `server_class` initialized with a `validator_class` object """ server_class = cls.get_server_class() validator_class = cls.get_validator_class() return server_class(validator_class()) @classmethod def get_oauthlib_core(cls): """ Cache and return `OAuthlibCore` instance so it will be created only on first request """ if not hasattr(cls, "_oauthlib_core"): server = cls.get_server() core_class = cls.get_oauthlib_backend_class() cls._oauthlib_core = core_class(server) return cls._oauthlib_core def validate_authorization_request(self, request): """ A wrapper method that calls validate_authorization_request on `server_class` instance. :param request: The current django.http.HttpRequest object """ core = self.get_oauthlib_core() return core.validate_authorization_request(request) def create_authorization_response(self, request, scopes, credentials, allow): """ A wrapper method that calls create_authorization_response on `server_class` instance. :param request: The current django.http.HttpRequest object :param scopes: A space-separated string of provided scopes :param credentials: Authorization credentials dictionary containing `client_id`, `state`, `redirect_uri`, `response_type` :param allow: True if the user authorize the client, otherwise False """ # TODO: move this scopes conversion from and to string into a utils function scopes = scopes.split(" ") if scopes else [] core = self.get_oauthlib_core() return core.create_authorization_response(request, scopes, credentials, allow) def create_token_response(self, request): """ A wrapper method that calls create_token_response on `server_class` instance. :param request: The current django.http.HttpRequest object """ core = self.get_oauthlib_core() return core.create_token_response(request) def create_revocation_response(self, request): """ A wrapper method that calls create_revocation_response on the `server_class` instance. :param request: The current django.http.HttpRequest object """ core = self.get_oauthlib_core() return core.create_revocation_response(request) def verify_request(self, request): """ A wrapper method that calls verify_request on `server_class` instance. :param request: The current django.http.HttpRequest object """ core = self.get_oauthlib_core() return core.verify_request(request, scopes=self.get_scopes()) def get_scopes(self): """ This should return the list of scopes required to access the resources. By default it returns an empty list. """ return [] def error_response(self, error, **kwargs): """ Return an error to be displayed to the resource owner if anything goes awry. :param error: :attr:`OAuthToolkitError` """ oauthlib_error = error.oauthlib_error redirect_uri = oauthlib_error.redirect_uri or "" separator = "&" if "?" in redirect_uri else "?" error_response = { "error": oauthlib_error, "url": redirect_uri + separator + oauthlib_error.urlencoded, } error_response.update(kwargs) # If we got a malicious redirect_uri or client_id, we will *not* redirect back to the URL. if isinstance(error, FatalClientError): redirect = False else: redirect = True return redirect, error_response class ScopedResourceMixin(object): """ Helper mixin that implements "scopes handling" behaviour """ required_scopes = None def get_scopes(self, *args, **kwargs): """ Return the scopes needed to access the resource :param args: Support scopes injections from the outside (not yet implemented) """ if self.required_scopes is None: raise ImproperlyConfigured( "ProtectedResourceMixin requires either a definition of 'required_scopes'" " or an implementation of 'get_scopes()'") else: return self.required_scopes class ProtectedResourceMixin(OAuthLibMixin): """ Helper mixin that implements OAuth2 protection on request dispatch, specially useful for Django Generic Views """ def dispatch(self, request, *args, **kwargs): # let preflight OPTIONS requests pass if request.method.upper() == "OPTIONS": return super(ProtectedResourceMixin, self).dispatch(request, *args, **kwargs) # check if the request is valid and the protected resource may be accessed valid, r = self.verify_request(request) if valid: request.resource_owner = r.user return super(ProtectedResourceMixin, self).dispatch(request, *args, **kwargs) else: return HttpResponseForbidden() class ReadWriteScopedResourceMixin(ScopedResourceMixin, OAuthLibMixin): """ Helper mixin that implements "read and write scopes" behavior """ required_scopes = [] read_write_scope = None def __new__(cls, *args, **kwargs): provided_scopes = get_scopes_backend().get_all_scopes() read_write_scopes = [oauth2_settings.READ_SCOPE, oauth2_settings.WRITE_SCOPE] if not set(read_write_scopes).issubset(set(provided_scopes)): raise ImproperlyConfigured( "ReadWriteScopedResourceMixin requires following scopes {}" ' to be in OAUTH2_PROVIDER["SCOPES"] list in settings'.format(read_write_scopes) ) return super(ReadWriteScopedResourceMixin, cls).__new__(cls, *args, **kwargs) def dispatch(self, request, *args, **kwargs): if request.method.upper() in SAFE_HTTP_METHODS: self.read_write_scope = oauth2_settings.READ_SCOPE else: self.read_write_scope = oauth2_settings.WRITE_SCOPE return super(ReadWriteScopedResourceMixin, self).dispatch(request, *args, **kwargs) def get_scopes(self, *args, **kwargs): scopes = super(ReadWriteScopedResourceMixin, self).get_scopes(*args, **kwargs) # this returns a copy so that self.required_scopes is not modified return scopes + [self.read_write_scope] django-oauth-toolkit-1.0.0/oauth2_provider/views/token.py000066400000000000000000000023071311555211500235170ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy from django.views.generic import DeleteView, ListView from ..models import get_access_token_model class AuthorizedTokensListView(LoginRequiredMixin, ListView): """ Show a page where the current logged-in user can see his tokens so they can revoke them """ context_object_name = "authorized_tokens" template_name = "oauth2_provider/authorized-tokens.html" model = get_access_token_model() def get_queryset(self): """ Show only user"s tokens """ return super(AuthorizedTokensListView, self).get_queryset()\ .select_related("application").filter(user=self.request.user) class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView): """ View for revoking a specific token """ template_name = "oauth2_provider/authorized-token-delete.html" success_url = reverse_lazy("oauth2_provider:authorized-token-list") model = get_access_token_model() def get_queryset(self): return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user) django-oauth-toolkit-1.0.0/setup.cfg000066400000000000000000000020621311555211500173730ustar00rootroot00000000000000[metadata] name = django-oauth-toolkit version = 1.0.0 description = OAuth2 Provider for Django author = Federico Frenguelli, Massimiliano Pippi author_email = synasius@gmail.com url = https://github.com/evonove/django-oauth-toolkit download_url = https://github.com/evonove/django-oauth-toolkit/tarball/master keywords = django, oauth, oauth2, oauthlib classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Framework :: Django Framework :: Django :: 1.10 Framework :: Django :: 1.11 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Topic :: Internet :: WWW/HTTP [options] packages = find: include_package_data = True zip_safe = False install_requires = django >= 1.10 oauthlib >= 2.0.1 requests >= 2.13.0 [options.packages.find] exclude = tests [bdist_wheel] universal = 1 django-oauth-toolkit-1.0.0/setup.py000077500000000000000000000000761311555211500172720ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup() django-oauth-toolkit-1.0.0/tests/000077500000000000000000000000001311555211500167145ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/tests/__init__.py000066400000000000000000000000001311555211500210130ustar00rootroot00000000000000django-oauth-toolkit-1.0.0/tests/models.py000066400000000000000000000010411311555211500205450ustar00rootroot00000000000000from django.db import models from oauth2_provider.models import ( AbstractAccessToken, AbstractApplication, AbstractGrant, AbstractRefreshToken ) class SampleApplication(AbstractApplication): custom_field = models.CharField(max_length=255) class SampleAccessToken(AbstractAccessToken): custom_field = models.CharField(max_length=255) class SampleRefreshToken(AbstractRefreshToken): custom_field = models.CharField(max_length=255) class SampleGrant(AbstractGrant): custom_field = models.CharField(max_length=255) django-oauth-toolkit-1.0.0/tests/settings.py000066400000000000000000000057361311555211500211410ustar00rootroot00000000000000ADMINS = () MANAGERS = ADMINS DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": "example.sqlite", } } ALLOWED_HOSTS = [] TIME_ZONE = "UTC" LANGUAGE_CODE = "en-us" SITE_ID = 1 USE_I18N = True USE_L10N = True USE_TZ = True MEDIA_ROOT = "" MEDIA_URL = "" STATIC_ROOT = "" STATIC_URL = "/static/" STATICFILES_DIRS = () STATICFILES_FINDERS = ( "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) # Make this unique, and don"t share it with anybody. SECRET_KEY = "1234567890evonove" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "debug": True, "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", ], }, }, ] MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ) ROOT_URLCONF = "tests.urls" INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.staticfiles", "django.contrib.admin", "oauth2_provider", "tests", ) LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s" }, "simple": { "format": "%(levelname)s %(message)s" }, }, "filters": { "require_debug_false": { "()": "django.utils.log.RequireDebugFalse" } }, "handlers": { "mail_admins": { "level": "ERROR", "filters": ["require_debug_false"], "class": "django.utils.log.AdminEmailHandler" }, "console": { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "simple" }, "null": { "level": "DEBUG", "class": "logging.NullHandler", }, }, "loggers": { "django.request": { "handlers": ["mail_admins"], "level": "ERROR", "propagate": True, }, "oauth2_provider": { "handlers": ["null"], "level": "DEBUG", "propagate": True, }, } } django-oauth-toolkit-1.0.0/tests/test_application_views.py000066400000000000000000000100151311555211500240420ustar00rootroot00000000000000from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from oauth2_provider.models import get_application_model from oauth2_provider.settings import oauth2_settings from oauth2_provider.views.application import ApplicationRegistration from .models import SampleApplication Application = get_application_model() UserModel = get_user_model() class BaseTest(TestCase): def setUp(self): self.foo_user = UserModel.objects.create_user("foo_user", "test@example.com", "123456") self.bar_user = UserModel.objects.create_user("bar_user", "dev@example.com", "123456") def tearDown(self): self.foo_user.delete() self.bar_user.delete() class TestApplicationRegistrationView(BaseTest): def test_get_form_class(self): """ Tests that the form class returned by the "get_form_class" method is bound to custom application model defined in the "OAUTH2_PROVIDER_APPLICATION_MODEL" setting. """ # Patch oauth2 settings to use a custom Application model oauth2_settings.APPLICATION_MODEL = "tests.SampleApplication" # Create a registration view and tests that the model form is bound # to the custom Application model application_form_class = ApplicationRegistration().get_form_class() self.assertEqual(SampleApplication, application_form_class._meta.model) # Revert oauth2 settings oauth2_settings.APPLICATION_MODEL = "oauth2_provider.Application" def test_application_registration_user(self): self.client.login(username="foo_user", password="123456") form_data = { "name": "Foo app", "client_id": "client_id", "client_secret": "client_secret", "client_type": Application.CLIENT_CONFIDENTIAL, "redirect_uris": "http://example.com", "authorization_grant_type": Application.GRANT_AUTHORIZATION_CODE, } response = self.client.post(reverse("oauth2_provider:register"), form_data) self.assertEqual(response.status_code, 302) app = get_application_model().objects.get(name="Foo app") self.assertEqual(app.user.username, "foo_user") class TestApplicationViews(BaseTest): def _create_application(self, name, user): app = Application.objects.create( name=name, redirect_uris="http://example.com", client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, user=user ) return app def setUp(self): super(TestApplicationViews, self).setUp() self.app_foo_1 = self._create_application("app foo_user 1", self.foo_user) self.app_foo_2 = self._create_application("app foo_user 2", self.foo_user) self.app_foo_3 = self._create_application("app foo_user 3", self.foo_user) self.app_bar_1 = self._create_application("app bar_user 1", self.bar_user) self.app_bar_2 = self._create_application("app bar_user 2", self.bar_user) def tearDown(self): super(TestApplicationViews, self).tearDown() get_application_model().objects.all().delete() def test_application_list(self): self.client.login(username="foo_user", password="123456") response = self.client.get(reverse("oauth2_provider:list")) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["object_list"]), 3) def test_application_detail_owner(self): self.client.login(username="foo_user", password="123456") response = self.client.get(reverse("oauth2_provider:detail", args=(self.app_foo_1.pk,))) self.assertEqual(response.status_code, 200) def test_application_detail_not_owner(self): self.client.login(username="foo_user", password="123456") response = self.client.get(reverse("oauth2_provider:detail", args=(self.app_bar_1.pk,))) self.assertEqual(response.status_code, 404) django-oauth-toolkit-1.0.0/tests/test_auth_backends.py000066400000000000000000000113111311555211500231150ustar00rootroot00000000000000from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.http import HttpResponse from django.test import RequestFactory, TestCase from django.test.utils import modify_settings, override_settings from django.utils.timezone import now, timedelta from oauth2_provider.backends import OAuth2Backend from oauth2_provider.middleware import OAuth2TokenMiddleware from oauth2_provider.models import ( get_access_token_model, get_application_model, ) UserModel = get_user_model() ApplicationModel = get_application_model() AccessTokenModel = get_access_token_model() class BaseTest(TestCase): """ Base class for cases in this module """ def setUp(self): self.user = UserModel.objects.create_user("user", "test@example.com", "123456") self.app = ApplicationModel.objects.create( name="app", client_type=ApplicationModel.CLIENT_CONFIDENTIAL, authorization_grant_type=ApplicationModel.GRANT_CLIENT_CREDENTIALS, user=self.user ) self.token = AccessTokenModel.objects.create( user=self.user, token="tokstr", application=self.app, expires=now() + timedelta(days=365) ) self.factory = RequestFactory() def tearDown(self): self.user.delete() self.app.delete() self.token.delete() class TestOAuth2Backend(BaseTest): def test_authenticate(self): auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "tokstr", } request = self.factory.get("/a-resource", **auth_headers) backend = OAuth2Backend() credentials = {"request": request} u = backend.authenticate(**credentials) self.assertEqual(u, self.user) def test_authenticate_fail(self): auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "badstring", } request = self.factory.get("/a-resource", **auth_headers) backend = OAuth2Backend() credentials = {"request": request} self.assertIsNone(backend.authenticate(**credentials)) credentials = {"username": "u", "password": "p"} self.assertIsNone(backend.authenticate(**credentials)) def test_get_user(self): backend = OAuth2Backend() self.assertEqual(self.user, backend.get_user(self.user.pk)) self.assertIsNone(backend.get_user(123456)) @override_settings( AUTHENTICATION_BACKENDS=( "oauth2_provider.backends.OAuth2Backend", "django.contrib.auth.backends.ModelBackend", ), ) @modify_settings( MIDDLEWARE={ "append": "oauth2_provider.middleware.OAuth2TokenMiddleware", } ) class TestOAuth2Middleware(BaseTest): def setUp(self): super(TestOAuth2Middleware, self).setUp() self.anon_user = AnonymousUser() def test_middleware_wrong_headers(self): m = OAuth2TokenMiddleware() request = self.factory.get("/a-resource") self.assertIsNone(m.process_request(request)) auth_headers = { "HTTP_AUTHORIZATION": "Beerer " + "badstring", # a Beer token for you! } request = self.factory.get("/a-resource", **auth_headers) self.assertIsNone(m.process_request(request)) def test_middleware_user_is_set(self): m = OAuth2TokenMiddleware() auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "tokstr", } request = self.factory.get("/a-resource", **auth_headers) request.user = self.user self.assertIsNone(m.process_request(request)) request.user = self.anon_user self.assertIsNone(m.process_request(request)) def test_middleware_success(self): m = OAuth2TokenMiddleware() auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "tokstr", } request = self.factory.get("/a-resource", **auth_headers) m.process_request(request) self.assertEqual(request.user, self.user) def test_middleware_response(self): m = OAuth2TokenMiddleware() auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "tokstr", } request = self.factory.get("/a-resource", **auth_headers) response = HttpResponse() processed = m.process_response(request, response) self.assertIs(response, processed) def test_middleware_response_header(self): m = OAuth2TokenMiddleware() auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "tokstr", } request = self.factory.get("/a-resource", **auth_headers) response = HttpResponse() m.process_response(request, response) self.assertIn("Vary", response) self.assertIn("Authorization", response["Vary"]) django-oauth-toolkit-1.0.0/tests/test_authorization_code.py000066400000000000000000001242331311555211500242240ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import datetime import json from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase from django.urls import reverse from django.utils import timezone from oauth2_provider.compat import parse_qs, urlencode, urlparse from oauth2_provider.models import ( get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model, ) from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ProtectedResourceView from .utils import get_basic_auth_header Application = get_application_model() AccessToken = get_access_token_model() Grant = get_grant_model() RefreshToken = get_refresh_token_model() UserModel = get_user_model() # mocking a protected resource view class ResourceView(ProtectedResourceView): def get(self, request, *args, **kwargs): return "This is a protected resource" class BaseTest(TestCase): def setUp(self): self.factory = RequestFactory() self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["http", "custom-scheme"] self.application = Application( name="Test Application", redirect_uris=( "http://localhost http://example.com http://example.org custom-scheme://example.com" ), user=self.dev_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.application.save() oauth2_settings._SCOPES = ["read", "write"] oauth2_settings._DEFAULT_SCOPES = ["read", "write"] def tearDown(self): self.application.delete() self.test_user.delete() self.dev_user.delete() class TestRegressionIssue315(BaseTest): """ Test to avoid regression for the issue 315: request object was being reassigned when getting AuthorizationView """ def test_request_is_not_overwritten(self): self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) assert "request" not in response.context_data class TestAuthorizationCodeView(BaseTest): def test_skip_authorization_completely(self): """ If application.skip_authorization = True, should skip the authorization page. """ self.client.login(username="test_user", password="123456") self.application.skip_authorization = True self.application.save() query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 302) def test_pre_auth_invalid_client(self): """ Test error for an invalid client_id with response_type: code """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": "fakeclientid", "response_type": "code", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 400) self.assertEqual( response.context_data["url"], "?error=invalid_request&error_description=Invalid+client_id+parameter+value." ) def test_pre_auth_valid_client(self): """ Test response for a valid client_id with response_type: code """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) # check form is in context and form params are valid self.assertIn("form", response.context) form = response.context["form"] self.assertEqual(form["redirect_uri"].value(), "http://example.org") self.assertEqual(form["state"].value(), "random_state_string") self.assertEqual(form["scope"].value(), "read write") self.assertEqual(form["client_id"].value(), self.application.client_id) def test_pre_auth_valid_client_custom_redirect_uri_scheme(self): """ Test response for a valid client_id with response_type: code using a non-standard, but allowed, redirect_uri scheme. """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "custom-scheme://example.com", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) # check form is in context and form params are valid self.assertIn("form", response.context) form = response.context["form"] self.assertEqual(form["redirect_uri"].value(), "custom-scheme://example.com") self.assertEqual(form["state"].value(), "random_state_string") self.assertEqual(form["scope"].value(), "read write") self.assertEqual(form["client_id"].value(), self.application.client_id) def test_pre_auth_approval_prompt(self): tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "approval_prompt": "auto", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 302) # user already authorized the application, but with different scopes: prompt them. tok.scope = "read" tok.save() response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_pre_auth_approval_prompt_default(self): self.assertEqual(oauth2_settings.REQUEST_APPROVAL_PROMPT, "force") AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_pre_auth_approval_prompt_default_override(self): oauth2_settings.REQUEST_APPROVAL_PROMPT = "auto" AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 302) def test_pre_auth_default_redirect(self): """ Test for default redirect uri if omitted from query string with response_type: code """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) form = response.context["form"] self.assertEqual(form["redirect_uri"].value(), "http://localhost") def test_pre_auth_forbibben_redirect(self): """ Test error when passing a forbidden redirect_uri in query string with response_type: code """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "redirect_uri": "http://forbidden.it", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_pre_auth_wrong_response_type(self): """ Test error when passing a wrong response_type in query string """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "WRONG", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 302) self.assertIn("error=unsupported_response_type", response["Location"]) def test_code_post_auth_allow(self): """ Test authorization code is given for an allowed request with response_type: code """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("http://example.org?", response["Location"]) self.assertIn("state=random_state_string", response["Location"]) self.assertIn("code=", response["Location"]) def test_code_post_auth_deny(self): """ Test error when resource owner deny access """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "code", "allow": False, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("error=access_denied", response["Location"]) def test_code_post_auth_bad_responsetype(self): """ Test authorization code is given for an allowed request with a response_type not supported """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "UNKNOWN", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("http://example.org?error", response["Location"]) def test_code_post_auth_forbidden_redirect_uri(self): """ Test authorization code is given for an allowed request with a forbidden redirect_uri """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://forbidden.it", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 400) def test_code_post_auth_malicious_redirect_uri(self): """ Test validation of a malicious redirect_uri """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "/../", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 400) def test_code_post_auth_allow_custom_redirect_uri_scheme(self): """ Test authorization code is given for an allowed request with response_type: code using a non-standard, but allowed, redirect_uri scheme. """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "custom-scheme://example.com", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("custom-scheme://example.com?", response["Location"]) self.assertIn("state=random_state_string", response["Location"]) self.assertIn("code=", response["Location"]) def test_code_post_auth_deny_custom_redirect_uri_scheme(self): """ Test error when resource owner deny access using a non-standard, but allowed, redirect_uri scheme. """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "custom-scheme://example.com", "response_type": "code", "allow": False, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("custom-scheme://example.com?", response["Location"]) self.assertIn("error=access_denied", response["Location"]) def test_code_post_auth_redirection_uri_with_querystring(self): """ Tests that a redirection uri with query string is allowed and query string is retained on redirection. See http://tools.ietf.org/html/rfc6749#section-3.1.2 """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.com?foo=bar", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("http://example.com?foo=bar", response["Location"]) self.assertIn("code=", response["Location"]) def test_code_post_auth_failing_redirection_uri_with_querystring(self): """ Test that in case of error the querystring of the redirection uri is preserved See https://github.com/evonove/django-oauth-toolkit/issues/238 """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.com?foo=bar", "response_type": "code", "allow": False, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertEqual("http://example.com?foo=bar&error=access_denied", response["Location"]) def test_code_post_auth_fails_when_redirect_uri_path_is_invalid(self): """ Tests that a redirection uri is matched using scheme + netloc + path """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.com/a?foo=bar", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 400) class TestAuthorizationCodeTokenView(BaseTest): def get_auth(self): """ Helper method to retrieve a valid authorization code """ authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) return query_dict["code"].pop() def test_basic_auth(self): """ Request an access token using basic authentication for client authentication """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["token_type"], "Bearer") self.assertEqual(content["scope"], "read write") self.assertEqual(content["expires_in"], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) def test_refresh(self): """ Request an access token using a refresh token """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) self.assertTrue("refresh_token" in content) # make a second token request to be sure the previous refresh token remains valid, see #65 authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) token_request_data = { "grant_type": "refresh_token", "refresh_token": content["refresh_token"], "scope": content["scope"], } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertTrue("access_token" in content) # check refresh token cannot be used twice response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) content = json.loads(response.content.decode("utf-8")) self.assertTrue("invalid_grant" in content.values()) def test_refresh_invalidates_old_tokens(self): """ Ensure existing refresh tokens are cleaned up when issuing new ones """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) rt = content["refresh_token"] at = content["access_token"] token_request_data = { "grant_type": "refresh_token", "refresh_token": rt, "scope": content["scope"], } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) self.assertFalse(RefreshToken.objects.filter(token=rt).exists()) self.assertFalse(AccessToken.objects.filter(token=at).exists()) def test_refresh_no_scopes(self): """ Request an access token using a refresh token without passing any scope """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) self.assertTrue("refresh_token" in content) token_request_data = { "grant_type": "refresh_token", "refresh_token": content["refresh_token"], } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertTrue("access_token" in content) def test_refresh_bad_scopes(self): """ Request an access token using a refresh token and wrong scopes """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) self.assertTrue("refresh_token" in content) token_request_data = { "grant_type": "refresh_token", "refresh_token": content["refresh_token"], "scope": "read write nuke", } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_refresh_fail_repeating_requests(self): """ Try refreshing an access token with the same refresh token more than once """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) self.assertTrue("refresh_token" in content) token_request_data = { "grant_type": "refresh_token", "refresh_token": content["refresh_token"], "scope": content["scope"], } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_refresh_repeating_requests_non_rotating_tokens(self): """ Try refreshing an access token with the same refresh token more than once when not rotating tokens. """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) self.assertTrue("refresh_token" in content) token_request_data = { "grant_type": "refresh_token", "refresh_token": content["refresh_token"], "scope": content["scope"], } oauth2_settings.ROTATE_REFRESH_TOKEN = False response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) oauth2_settings.ROTATE_REFRESH_TOKEN = True def test_basic_auth_bad_authcode(self): """ Request an access token using a bad authorization code """ self.client.login(username="test_user", password="123456") token_request_data = { "grant_type": "authorization_code", "code": "BLAH", "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_basic_auth_bad_granttype(self): """ Request an access token using a bad grant_type string """ self.client.login(username="test_user", password="123456") token_request_data = { "grant_type": "UNKNOWN", "code": "BLAH", "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 400) def test_basic_auth_grant_expired(self): """ Request an access token using an expired grant token """ self.client.login(username="test_user", password="123456") g = Grant( application=self.application, user=self.test_user, code="BLAH", expires=timezone.now(), redirect_uri="", scope="") g.save() token_request_data = { "grant_type": "authorization_code", "code": "BLAH", "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_basic_auth_bad_secret(self): """ Request an access token using basic authentication for client authentication """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, "BOOM!") response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_basic_auth_wrong_auth_type(self): """ Request an access token using basic authentication for client authentication """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } user_pass = "{0}:{1}".format(self.application.client_id, self.application.client_secret) auth_string = base64.b64encode(user_pass.encode("utf-8")) auth_headers = { "HTTP_AUTHORIZATION": "Wrong " + auth_string.decode("utf-8"), } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_request_body_params(self): """ Request an access token using client_type: public """ self.client.login(username="test_user", password="123456") authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org", "client_id": self.application.client_id, "client_secret": self.application.client_secret, } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["token_type"], "Bearer") self.assertEqual(content["scope"], "read write") self.assertEqual(content["expires_in"], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) def test_public(self): """ Request an access token using client_type: public """ self.client.login(username="test_user", password="123456") self.application.client_type = Application.CLIENT_PUBLIC self.application.save() authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org", "client_id": self.application.client_id } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["token_type"], "Bearer") self.assertEqual(content["scope"], "read write") self.assertEqual(content["expires_in"], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) def test_malicious_redirect_uri(self): """ Request an access token using client_type: public and ensure redirect_uri is properly validated. """ self.client.login(username="test_user", password="123456") self.application.client_type = Application.CLIENT_PUBLIC self.application.save() authorization_code = self.get_auth() token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "/../", "client_id": self.application.client_id } response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data) self.assertEqual(response.status_code, 401) def test_code_exchange_succeed_when_redirect_uri_match(self): """ Tests code exchange succeed when redirect uri matches the one used for code request """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org?foo=bar", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org?foo=bar" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["token_type"], "Bearer") self.assertEqual(content["scope"], "read write") self.assertEqual(content["expires_in"], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) def test_code_exchange_fails_when_redirect_uri_does_not_match(self): """ Tests code exchange fails when redirect uri does not match the one used for code request """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org?foo=bar", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org?foo=baraa" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) def test_code_exchange_succeed_when_redirect_uri_match_with_multiple_query_params(self): """ Tests code exchange succeed when redirect uri matches the one used for code request """ self.client.login(username="test_user", password="123456") self.application.redirect_uris = "http://localhost http://example.com?foo=bar" self.application.save() # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.com?bar=baz&foo=bar", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.com?bar=baz&foo=bar" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["token_type"], "Bearer") self.assertEqual(content["scope"], "read write") self.assertEqual(content["expires_in"], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) class TestAuthorizationCodeProtectedResource(BaseTest): def test_resource_access_allowed(self): self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") def test_resource_access_deny(self): auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "faketoken", } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ResourceView.as_view() response = view(request) self.assertEqual(response.status_code, 403) class TestDefaultScopes(BaseTest): def test_pre_auth_default_scopes(self): """ Test response for a valid client_id with response_type: code using default scopes """ self.client.login(username="test_user", password="123456") oauth2_settings._DEFAULT_SCOPES = ["read"] query_string = urlencode({ "client_id": self.application.client_id, "response_type": "code", "state": "random_state_string", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) # check form is in context and form params are valid self.assertIn("form", response.context) form = response.context["form"] self.assertEqual(form["redirect_uri"].value(), "http://example.org") self.assertEqual(form["state"].value(), "random_state_string") self.assertEqual(form["scope"].value(), "read") self.assertEqual(form["client_id"].value(), self.application.client_id) oauth2_settings._DEFAULT_SCOPES = ["read", "write"] django-oauth-toolkit-1.0.0/tests/test_client_credential.py000066400000000000000000000155331311555211500240040ustar00rootroot00000000000000from __future__ import unicode_literals import json from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase from django.urls import reverse from django.views.generic import View from oauthlib.oauth2 import BackendApplicationServer from oauth2_provider.compat import quote_plus from oauth2_provider.models import ( get_access_token_model, get_application_model, ) from oauth2_provider.oauth2_backends import OAuthLibCore from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ProtectedResourceView from oauth2_provider.views.mixins import OAuthLibMixin from .utils import get_basic_auth_header Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() # mocking a protected resource view class ResourceView(ProtectedResourceView): def get(self, request, *args, **kwargs): return "This is a protected resource" class BaseTest(TestCase): def setUp(self): self.factory = RequestFactory() self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") self.application = Application( name="test_client_credentials_app", user=self.dev_user, client_type=Application.CLIENT_PUBLIC, authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS, ) self.application.save() oauth2_settings._SCOPES = ["read", "write"] oauth2_settings._DEFAULT_SCOPES = ["read", "write"] def tearDown(self): self.application.delete() self.test_user.delete() self.dev_user.delete() class TestClientCredential(BaseTest): def test_client_credential_access_allowed(self): """ Request an access token using Client Credential Flow """ token_request_data = { "grant_type": "client_credentials", } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") def test_client_credential_does_not_issue_refresh_token(self): token_request_data = { "grant_type": "client_credentials", } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertNotIn("refresh_token", content) def test_client_credential_user_is_none_on_access_token(self): token_request_data = {"grant_type": "client_credentials"} auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) access_token = AccessToken.objects.get(token=content["access_token"]) self.assertIsNone(access_token.user) class TestExtendedRequest(BaseTest): @classmethod def setUpClass(cls): cls.request_factory = RequestFactory() super(TestExtendedRequest, cls).setUpClass() def test_extended_request(self): class TestView(OAuthLibMixin, View): server_class = BackendApplicationServer validator_class = OAuth2Validator oauthlib_backend_class = OAuthLibCore def get_scopes(self): return ["read", "write"] token_request_data = { "grant_type": "client_credentials", } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.request_factory.get("/fake-req", **auth_headers) request.user = "fake" test_view = TestView() self.assertIsInstance(test_view.get_server(), BackendApplicationServer) valid, r = test_view.verify_request(request) self.assertTrue(valid) self.assertIsNone(r.user) self.assertEqual(r.client, self.application) self.assertEqual(r.scopes, ["read", "write"]) class TestClientResourcePasswordBased(BaseTest): def test_client_resource_password_based(self): """ Request an access token using Resource Owner Password Based flow """ self.application.delete() self.application = Application( name="test_client_credentials_app", user=self.dev_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_PASSWORD, ) self.application.save() token_request_data = { "grant_type": "password", "username": "test_user", "password": "123456" } auth_headers = get_basic_auth_header( quote_plus(self.application.client_id), quote_plus(self.application.client_secret) ) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") django-oauth-toolkit-1.0.0/tests/test_decorators.py000066400000000000000000000065251311555211500225020ustar00rootroot00000000000000from datetime import timedelta from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase from django.utils import timezone from oauth2_provider.decorators import protected_resource, rw_protected_resource from oauth2_provider.models import ( get_access_token_model, get_application_model, ) from oauth2_provider.settings import oauth2_settings Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() class TestProtectedResourceDecorator(TestCase): @classmethod def setUpClass(cls): cls.request_factory = RequestFactory() super(TestProtectedResourceDecorator, cls).setUpClass() def setUp(self): self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.application = Application.objects.create( name="test_client_credentials_app", user=self.user, client_type=Application.CLIENT_PUBLIC, authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS, ) self.access_token = AccessToken.objects.create( user=self.user, scope="read write", expires=timezone.now() + timedelta(seconds=300), token="secret-access-token-key", application=self.application ) oauth2_settings._SCOPES = ["read", "write"] def test_access_denied(self): @protected_resource() def view(request, *args, **kwargs): return "protected contents" request = self.request_factory.get("/fake-resource") response = view(request) self.assertEqual(response.status_code, 403) def test_access_allowed(self): @protected_resource() def view(request, *args, **kwargs): return "protected contents" @protected_resource(scopes=["can_touch_this"]) def scoped_view(request, *args, **kwargs): return "moar protected contents" auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.access_token.token, } request = self.request_factory.get("/fake-resource", **auth_headers) response = view(request) self.assertEqual(response, "protected contents") # now with scopes self.access_token.scope = "can_touch_this" self.access_token.save() auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.access_token.token, } request = self.request_factory.get("/fake-resource", **auth_headers) response = scoped_view(request) self.assertEqual(response, "moar protected contents") def test_rw_protected(self): self.access_token.scope = "exotic_scope write" self.access_token.save() auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.access_token.token, } @rw_protected_resource(scopes=["exotic_scope"]) def scoped_view(request, *args, **kwargs): return "other protected contents" request = self.request_factory.post("/fake-resource", **auth_headers) response = scoped_view(request) self.assertEqual(response, "other protected contents") request = self.request_factory.get("/fake-resource", **auth_headers) response = scoped_view(request) self.assertEqual(response.status_code, 403) django-oauth-toolkit-1.0.0/tests/test_generator.py000066400000000000000000000023211311555211500223110ustar00rootroot00000000000000from __future__ import unicode_literals from django.test import TestCase from oauth2_provider.generators import ( BaseHashGenerator, ClientIdGenerator, ClientSecretGenerator, generate_client_id, generate_client_secret ) from oauth2_provider.settings import oauth2_settings class MockHashGenerator(BaseHashGenerator): def hash(self): return 42 class TestGenerators(TestCase): def tearDown(self): oauth2_settings.CLIENT_ID_GENERATOR_CLASS = ClientIdGenerator oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS = ClientSecretGenerator def test_generate_client_id(self): g = oauth2_settings.CLIENT_ID_GENERATOR_CLASS() self.assertEqual(len(g.hash()), 40) oauth2_settings.CLIENT_ID_GENERATOR_CLASS = MockHashGenerator self.assertEqual(generate_client_id(), 42) def test_generate_secret_id(self): g = oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS() self.assertEqual(len(g.hash()), 128) oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS = MockHashGenerator self.assertEqual(generate_client_secret(), 42) def test_basegen_misuse(self): g = BaseHashGenerator() self.assertRaises(NotImplementedError, g.hash) django-oauth-toolkit-1.0.0/tests/test_implicit.py000066400000000000000000000243501311555211500221430ustar00rootroot00000000000000from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase from django.urls import reverse from oauth2_provider.compat import parse_qs, urlencode, urlparse from oauth2_provider.models import get_application_model from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ProtectedResourceView Application = get_application_model() UserModel = get_user_model() # mocking a protected resource view class ResourceView(ProtectedResourceView): def get(self, request, *args, **kwargs): return "This is a protected resource" class BaseTest(TestCase): def setUp(self): self.factory = RequestFactory() self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") self.application = Application( name="Test Implicit Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.dev_user, client_type=Application.CLIENT_PUBLIC, authorization_grant_type=Application.GRANT_IMPLICIT, ) self.application.save() oauth2_settings._SCOPES = ["read", "write"] oauth2_settings._DEFAULT_SCOPES = ["read"] def tearDown(self): self.application.delete() self.test_user.delete() self.dev_user.delete() class TestImplicitAuthorizationCodeView(BaseTest): def test_pre_auth_valid_client_default_scopes(self): """ Test response for a valid client_id with response_type: token and default_scopes """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "token", "state": "random_state_string", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertIn("form", response.context) form = response.context["form"] self.assertEqual(form["scope"].value(), "read") def test_pre_auth_valid_client(self): """ Test response for a valid client_id with response_type: token """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "token", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) # check form is in context and form params are valid self.assertIn("form", response.context) form = response.context["form"] self.assertEqual(form["redirect_uri"].value(), "http://example.org") self.assertEqual(form["state"].value(), "random_state_string") self.assertEqual(form["scope"].value(), "read write") self.assertEqual(form["client_id"].value(), self.application.client_id) def test_pre_auth_invalid_client(self): """ Test error for an invalid client_id with response_type: token """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": "fakeclientid", "response_type": "token", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_pre_auth_default_redirect(self): """ Test for default redirect uri if omitted from query string with response_type: token """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "token", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 200) form = response.context["form"] self.assertEqual(form["redirect_uri"].value(), "http://localhost") def test_pre_auth_forbibben_redirect(self): """ Test error when passing a forbidden redirect_uri in query string with response_type: token """ self.client.login(username="test_user", password="123456") query_string = urlencode({ "client_id": self.application.client_id, "response_type": "token", "redirect_uri": "http://forbidden.it", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_post_auth_allow(self): """ Test authorization code is given for an allowed request with response_type: token """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "token", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("http://example.org#", response["Location"]) self.assertIn("access_token=", response["Location"]) self.assertIn("state=random_state_string", response["Location"]) def test_skip_authorization_completely(self): """ If application.skip_authorization = True, should skip the authorization page. """ self.client.login(username="test_user", password="123456") self.application.skip_authorization = True self.application.save() query_string = urlencode({ "client_id": self.application.client_id, "response_type": "token", "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:authorize"), qs=query_string) response = self.client.get(url) self.assertEqual(response.status_code, 302) self.assertIn("http://example.org#", response["Location"]) self.assertIn("access_token=", response["Location"]) self.assertIn("state=random_state_string", response["Location"]) def test_token_post_auth_deny(self): """ Test error when resource owner deny access """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "token", "allow": False, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("error=access_denied", response["Location"]) def test_implicit_redirection_uri_with_querystring(self): """ Tests that a redirection uri with query string is allowed and query string is retained on redirection. See http://tools.ietf.org/html/rfc6749#section-3.1.2 """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.com?foo=bar", "response_type": "token", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 302) self.assertIn("http://example.com?foo=bar", response["Location"]) self.assertIn("access_token=", response["Location"]) def test_implicit_fails_when_redirect_uri_path_is_invalid(self): """ Tests that a redirection uri is matched using scheme + netloc + path """ self.client.login(username="test_user", password="123456") form_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.com/a?foo=bar", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=form_data) self.assertEqual(response.status_code, 400) class TestImplicitTokenView(BaseTest): def test_resource_access_allowed(self): self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "read write", "redirect_uri": "http://example.org", "response_type": "token", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) # within implicit grant, access token is in the url fragment frag_dict = parse_qs(urlparse(response["Location"]).fragment) access_token = frag_dict["access_token"].pop() # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") django-oauth-toolkit-1.0.0/tests/test_introspection_auth.py000066400000000000000000000167471311555211500242650ustar00rootroot00000000000000from __future__ import unicode_literals import calendar import datetime from django.conf.urls import include, url from django.contrib.auth import get_user_model from django.http import HttpResponse from django.test import override_settings, TestCase from django.utils import timezone from oauthlib.common import Request from oauth2_provider.models import get_access_token_model, get_application_model from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ScopedProtectedResourceView try: from unittest import mock except ImportError: import mock Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() exp = datetime.datetime.now() + datetime.timedelta(days=1) class ScopeResourceView(ScopedProtectedResourceView): required_scopes = ["dolphin"] def get(self, request, *args, **kwargs): return HttpResponse("This is a protected resource", 200) def post(self, request, *args, **kwargs): return HttpResponse("This is a protected resource", 200) def mocked_requests_post(url, data, *args, **kwargs): """ Mock the response from the authentication server """ class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code def json(self): return self.json_data if "token" in data and data["token"] and data["token"] != "12345678900": return MockResponse({ "active": True, "scope": "read write dolphin", "client_id": "client_id_{}".format(data["token"]), "username": "{}_user".format(data["token"]), "exp": int(calendar.timegm(exp.timetuple())), }, 200) return MockResponse({ "active": False, }, 200) urlpatterns = [ url(r"^oauth2/", include("oauth2_provider.urls")), url(r"^oauth2-test-resource/$", ScopeResourceView.as_view()), ] @override_settings(ROOT_URLCONF=__name__) class TestTokenIntrospectionAuth(TestCase): """ Tests for Authorization through token introspection """ def setUp(self): self.validator = OAuth2Validator() self.request = mock.MagicMock(wraps=Request) self.resource_server_user = UserModel.objects.create_user( "resource_server", "test@example.com", "123456" ) self.application = Application( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.resource_server_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.application.save() self.resource_server_token = AccessToken.objects.create( user=self.resource_server_user, token="12345678900", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write introspection" ) self.invalid_token = AccessToken.objects.create( user=self.resource_server_user, token="12345678901", application=self.application, expires=timezone.now() + datetime.timedelta(days=-1), scope="read write dolphin" ) oauth2_settings._SCOPES = ["read", "write", "introspection", "dolphin"] oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL = "http://example.org/introspection" oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN = self.resource_server_token.token oauth2_settings.READ_SCOPE = "read" oauth2_settings.WRITE_SCOPE = "write" def tearDown(self): oauth2_settings._SCOPES = ["read", "write"] oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL = None oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN = None self.resource_server_token.delete() self.application.delete() AccessToken.objects.all().delete() UserModel.objects.all().delete() @mock.patch("requests.post", side_effect=mocked_requests_post) def test_get_token_from_authentication_server_not_existing_token(self, mock_get): """ Test method _get_token_from_authentication_server with non existing token """ token = self.validator._get_token_from_authentication_server( self.resource_server_token.token, oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL, oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN ) self.assertIsNone(token) @mock.patch("requests.post", side_effect=mocked_requests_post) def test_get_token_from_authentication_server_existing_token(self, mock_get): """ Test method _get_token_from_authentication_server with existing token """ token = self.validator._get_token_from_authentication_server( "foo", oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL, oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN ) self.assertIsInstance(token, AccessToken) self.assertEqual(token.user.username, "foo_user") self.assertEqual(token.scope, "read write dolphin") @mock.patch("requests.post", side_effect=mocked_requests_post) def test_validate_bearer_token(self, mock_get): """ Test method validate_bearer_token """ # with token = None self.assertFalse(self.validator.validate_bearer_token(None, ["dolphin"], self.request)) # with valid token and scope self.assertTrue( self.validator.validate_bearer_token( self.resource_server_token.token, ["introspection"], self.request ) ) # with initially invalid token, but validated through request self.assertTrue( self.validator.validate_bearer_token(self.invalid_token.token, ["dolphin"], self.request) ) # with locally unavailable token, but validated through request self.assertTrue(self.validator.validate_bearer_token("butzi", ["dolphin"], self.request)) # with valid token but invalid scope self.assertFalse(self.validator.validate_bearer_token("foo", ["kaudawelsch"], self.request)) # with token validated through request, but invalid scope self.assertFalse(self.validator.validate_bearer_token("butz", ["kaudawelsch"], self.request)) # with token validated through request and valid scope self.assertTrue(self.validator.validate_bearer_token("butzi", ["dolphin"], self.request)) @mock.patch("requests.post", side_effect=mocked_requests_post) def test_get_resource(self, mock_get): """ Test that we can access the resource with a get request and a remotely validated token """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer bar", } response = self.client.get("/oauth2-test-resource/", **auth_headers) self.assertEqual(response.content.decode("utf-8"), "This is a protected resource") @mock.patch("requests.post", side_effect=mocked_requests_post) def test_post_resource(self, mock_get): """ Test that we can access the resource with a post request and a remotely validated token """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer batz", } response = self.client.post("/oauth2-test-resource/", **auth_headers) self.assertEqual(response.content.decode("utf-8"), "This is a protected resource") django-oauth-toolkit-1.0.0/tests/test_introspection_view.py000066400000000000000000000224421311555211500242630ustar00rootroot00000000000000from __future__ import unicode_literals import calendar import datetime from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from django.utils import timezone from oauth2_provider.models import get_access_token_model, get_application_model from oauth2_provider.settings import oauth2_settings Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() class TestTokenIntrospectionViews(TestCase): """ Tests for Authorized Token Introspection Views """ def setUp(self): self.resource_server_user = UserModel.objects.create_user("resource_server", "test@example.com") self.test_user = UserModel.objects.create_user("bar_user", "dev@example.com") self.application = Application( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.test_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.application.save() self.resource_server_token = AccessToken.objects.create( user=self.resource_server_user, token="12345678900", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write introspection" ) self.valid_token = AccessToken.objects.create( user=self.test_user, token="12345678901", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write dolphin" ) self.invalid_token = AccessToken.objects.create( user=self.test_user, token="12345678902", application=self.application, expires=timezone.now() + datetime.timedelta(days=-1), scope="read write dolphin" ) self.token_without_user = AccessToken.objects.create( user=None, token="12345678903", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write dolphin" ) self.token_without_app = AccessToken.objects.create( user=self.test_user, token="12345678904", application=None, expires=timezone.now() + datetime.timedelta(days=1), scope="read write dolphin" ) oauth2_settings._SCOPES = ["read", "write", "introspection", "dolphin"] oauth2_settings.READ_SCOPE = "read" oauth2_settings.WRITE_SCOPE = "write" def tearDown(self): oauth2_settings._SCOPES = ["read", "write"] AccessToken.objects.all().delete() Application.objects.all().delete() UserModel.objects.all().delete() def test_view_forbidden(self): """ Test that the view is restricted for logged-in users. """ response = self.client.get(reverse("oauth2_provider:introspect")) self.assertEqual(response.status_code, 403) def test_view_get_valid_token(self): """ Test that when you pass a valid token as URL parameter, a json with an active token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.get( reverse("oauth2_provider:introspect"), {"token": self.valid_token.token}, **auth_headers) self.assertEqual(response.status_code, 200) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": True, "scope": self.valid_token.scope, "client_id": self.valid_token.application.client_id, "username": self.valid_token.user.get_username(), "exp": int(calendar.timegm(self.valid_token.expires.timetuple())), }) def test_view_get_valid_token_without_user(self): """ Test that when you pass a valid token as URL parameter, a json with an active token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.get( reverse("oauth2_provider:introspect"), {"token": self.token_without_user.token}, **auth_headers) self.assertEqual(response.status_code, 200) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": True, "scope": self.token_without_user.scope, "client_id": self.token_without_user.application.client_id, "exp": int(calendar.timegm(self.token_without_user.expires.timetuple())), }) def test_view_get_valid_token_without_app(self): """ Test that when you pass a valid token as URL parameter, a json with an active token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.get( reverse("oauth2_provider:introspect"), {"token": self.token_without_app.token}, **auth_headers) self.assertEqual(response.status_code, 200) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": True, "scope": self.token_without_app.scope, "username": self.token_without_app.user.get_username(), "exp": int(calendar.timegm(self.token_without_app.expires.timetuple())), }) def test_view_get_invalid_token(self): """ Test that when you pass an invalid token as URL parameter, a json with an inactive token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.get( reverse("oauth2_provider:introspect"), {"token": self.invalid_token.token}, **auth_headers) self.assertEqual(response.status_code, 200) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": False, }) def test_view_get_notexisting_token(self): """ Test that when you pass an non existing token as URL parameter, a json with an inactive token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.get( reverse("oauth2_provider:introspect"), {"token": "kaudawelsch"}, **auth_headers) self.assertEqual(response.status_code, 401) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": False, }) def test_view_post_valid_token(self): """ Test that when you pass a valid token as form parameter, a json with an active token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.post( reverse("oauth2_provider:introspect"), {"token": self.valid_token.token}, **auth_headers) self.assertEqual(response.status_code, 200) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": True, "scope": self.valid_token.scope, "client_id": self.valid_token.application.client_id, "username": self.valid_token.user.get_username(), "exp": int(calendar.timegm(self.valid_token.expires.timetuple())), }) def test_view_post_invalid_token(self): """ Test that when you pass an invalid token as form parameter, a json with an inactive token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.post( reverse("oauth2_provider:introspect"), {"token": self.invalid_token.token}, **auth_headers) self.assertEqual(response.status_code, 200) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": False, }) def test_view_post_notexisting_token(self): """ Test that when you pass an non existing token as form parameter, a json with an inactive token state is provided """ auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, } response = self.client.post( reverse("oauth2_provider:introspect"), {"token": "kaudawelsch"}, **auth_headers) self.assertEqual(response.status_code, 401) content = response.json() self.assertIsInstance(content, dict) self.assertDictEqual(content, { "active": False, }) django-oauth-toolkit-1.0.0/tests/test_mixins.py000066400000000000000000000063551311555211500216450ustar00rootroot00000000000000from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory, TestCase from django.views.generic import View from oauthlib.oauth2 import Server from oauth2_provider.oauth2_backends import OAuthLibCore from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.views.mixins import OAuthLibMixin, ProtectedResourceMixin, ScopedResourceMixin class BaseTest(TestCase): @classmethod def setUpClass(cls): cls.request_factory = RequestFactory() super(BaseTest, cls).setUpClass() class TestOAuthLibMixin(BaseTest): def test_missing_oauthlib_backend_class(self): class TestView(OAuthLibMixin, View): server_class = Server validator_class = OAuth2Validator test_view = TestView() self.assertRaises(ImproperlyConfigured, test_view.get_oauthlib_backend_class) def test_missing_server_class(self): class TestView(OAuthLibMixin, View): validator_class = OAuth2Validator oauthlib_backend_class = OAuthLibCore test_view = TestView() self.assertRaises(ImproperlyConfigured, test_view.get_server) def test_missing_validator_class(self): class TestView(OAuthLibMixin, View): server_class = Server oauthlib_backend_class = OAuthLibCore test_view = TestView() self.assertRaises(ImproperlyConfigured, test_view.get_server) def test_correct_server(self): class TestView(OAuthLibMixin, View): server_class = Server validator_class = OAuth2Validator oauthlib_backend_class = OAuthLibCore request = self.request_factory.get("/fake-req") request.user = "fake" test_view = TestView() self.assertIsInstance(test_view.get_server(), Server) def test_custom_backend(self): class AnotherOauthLibBackend(object): pass class TestView(OAuthLibMixin, View): server_class = Server validator_class = OAuth2Validator oauthlib_backend_class = AnotherOauthLibBackend request = self.request_factory.get("/fake-req") request.user = "fake" test_view = TestView() self.assertEqual( test_view.get_oauthlib_backend_class(), AnotherOauthLibBackend ) class TestScopedResourceMixin(BaseTest): def test_missing_required_scopes(self): class TestView(ScopedResourceMixin, View): pass test_view = TestView() self.assertRaises(ImproperlyConfigured, test_view.get_scopes) def test_correct_required_scopes(self): class TestView(ScopedResourceMixin, View): required_scopes = ["scope1", "scope2"] test_view = TestView() self.assertEqual(test_view.get_scopes(), ["scope1", "scope2"]) class TestProtectedResourceMixin(BaseTest): def test_options_shall_pass(self): class TestView(ProtectedResourceMixin, View): server_class = Server validator_class = OAuth2Validator request = self.request_factory.options("/fake-req") view = TestView.as_view() response = view(request) self.assertEqual(response.status_code, 200) django-oauth-toolkit-1.0.0/tests/test_models.py000066400000000000000000000254501311555211500216160ustar00rootroot00000000000000from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from oauth2_provider.models import ( get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model, ) from oauth2_provider.settings import oauth2_settings Application = get_application_model() Grant = get_grant_model() AccessToken = get_access_token_model() RefreshToken = get_refresh_token_model() UserModel = get_user_model() class TestModels(TestCase): def setUp(self): self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456") def test_allow_scopes(self): self.client.login(username="test_user", password="123456") app = Application.objects.create( name="test_app", redirect_uris="http://localhost http://example.com http://example.org", user=self.user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) access_token = AccessToken( user=self.user, scope="read write", expires=0, token="", application=app ) self.assertTrue(access_token.allow_scopes(["read", "write"])) self.assertTrue(access_token.allow_scopes(["write", "read"])) self.assertTrue(access_token.allow_scopes(["write", "read", "read"])) self.assertTrue(access_token.allow_scopes([])) self.assertFalse(access_token.allow_scopes(["write", "destroy"])) def test_grant_authorization_code_redirect_uris(self): app = Application( name="test_app", redirect_uris="", user=self.user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.assertRaises(ValidationError, app.full_clean) def test_grant_implicit_redirect_uris(self): app = Application( name="test_app", redirect_uris="", user=self.user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_IMPLICIT, ) self.assertRaises(ValidationError, app.full_clean) def test_str(self): app = Application( redirect_uris="", user=self.user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_IMPLICIT, ) self.assertEqual("%s" % app, app.client_id) app.name = "test_app" self.assertEqual("%s" % app, "test_app") def test_scopes_property(self): self.client.login(username="test_user", password="123456") app = Application.objects.create( name="test_app", redirect_uris="http://localhost http://example.com http://example.org", user=self.user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) access_token = AccessToken( user=self.user, scope="read write", expires=0, token="", application=app ) access_token2 = AccessToken( user=self.user, scope="write", expires=0, token="", application=app ) self.assertEqual(access_token.scopes, {"read": "Reading scope", "write": "Writing scope"}) self.assertEqual(access_token2.scopes, {"write": "Writing scope"}) @override_settings( OAUTH2_PROVIDER_APPLICATION_MODEL="tests.SampleApplication", OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL="tests.SampleAccessToken", OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL="tests.SampleRefreshToken", OAUTH2_PROVIDER_GRANT_MODEL="tests.SampleGrant" ) class TestCustomModels(TestCase): def setUp(self): self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456") def test_custom_application_model(self): """ If a custom application model is installed, it should be present in the related objects and not the swapped out one. See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) """ related_object_names = [ f.name for f in UserModel._meta.get_fields() if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete ] self.assertNotIn("oauth2_provider:application", related_object_names) self.assertIn("tests_sampleapplication", related_object_names) def test_custom_application_model_incorrect_format(self): # Patch oauth2 settings to use a custom Application model oauth2_settings.APPLICATION_MODEL = "IncorrectApplicationFormat" self.assertRaises(ValueError, get_application_model) # Revert oauth2 settings oauth2_settings.APPLICATION_MODEL = "oauth2_provider.Application" def test_custom_application_model_not_installed(self): # Patch oauth2 settings to use a custom Application model oauth2_settings.APPLICATION_MODEL = "tests.ApplicationNotInstalled" self.assertRaises(LookupError, get_application_model) # Revert oauth2 settings oauth2_settings.APPLICATION_MODEL = "oauth2_provider.Application" def test_custom_access_token_model(self): """ If a custom access token model is installed, it should be present in the related objects and not the swapped out one. """ # Django internals caches the related objects. related_object_names = [ f.name for f in UserModel._meta.get_fields() if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete ] self.assertNotIn("oauth2_provider:access_token", related_object_names) self.assertIn("tests_sampleaccesstoken", related_object_names) def test_custom_access_token_model_incorrect_format(self): # Patch oauth2 settings to use a custom AccessToken model oauth2_settings.ACCESS_TOKEN_MODEL = "IncorrectAccessTokenFormat" self.assertRaises(ValueError, get_access_token_model) # Revert oauth2 settings oauth2_settings.ACCESS_TOKEN_MODEL = "oauth2_provider.AccessToken" def test_custom_access_token_model_not_installed(self): # Patch oauth2 settings to use a custom AccessToken model oauth2_settings.ACCESS_TOKEN_MODEL = "tests.AccessTokenNotInstalled" self.assertRaises(LookupError, get_access_token_model) # Revert oauth2 settings oauth2_settings.ACCESS_TOKEN_MODEL = "oauth2_provider.AccessToken" def test_custom_refresh_token_model(self): """ If a custom refresh token model is installed, it should be present in the related objects and not the swapped out one. """ # Django internals caches the related objects. related_object_names = [ f.name for f in UserModel._meta.get_fields() if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete ] self.assertNotIn("oauth2_provider:refresh_token", related_object_names) self.assertIn("tests_samplerefreshtoken", related_object_names) def test_custom_refresh_token_model_incorrect_format(self): # Patch oauth2 settings to use a custom RefreshToken model oauth2_settings.REFRESH_TOKEN_MODEL = "IncorrectRefreshTokenFormat" self.assertRaises(ValueError, get_refresh_token_model) # Revert oauth2 settings oauth2_settings.REFRESH_TOKEN_MODEL = "oauth2_provider.RefreshToken" def test_custom_refresh_token_model_not_installed(self): # Patch oauth2 settings to use a custom AccessToken model oauth2_settings.REFRESH_TOKEN_MODEL = "tests.RefreshTokenNotInstalled" self.assertRaises(LookupError, get_refresh_token_model) # Revert oauth2 settings oauth2_settings.REFRESH_TOKEN_MODEL = "oauth2_provider.RefreshToken" def test_custom_grant_model(self): """ If a custom grant model is installed, it should be present in the related objects and not the swapped out one. """ # Django internals caches the related objects. related_object_names = [ f.name for f in UserModel._meta.get_fields() if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete ] self.assertNotIn("oauth2_provider:grant", related_object_names) self.assertIn("tests_samplegrant", related_object_names) def test_custom_grant_model_incorrect_format(self): # Patch oauth2 settings to use a custom Grant model oauth2_settings.GRANT_MODEL = "IncorrectGrantFormat" self.assertRaises(ValueError, get_grant_model) # Revert oauth2 settings oauth2_settings.GRANT_MODEL = "oauth2_provider.Grant" def test_custom_grant_model_not_installed(self): # Patch oauth2 settings to use a custom AccessToken model oauth2_settings.GRANT_MODEL = "tests.GrantNotInstalled" self.assertRaises(LookupError, get_grant_model) # Revert oauth2 settings oauth2_settings.GRANT_MODEL = "oauth2_provider.Grant" class TestGrantModel(TestCase): def test_str(self): grant = Grant(code="test_code") self.assertEqual("%s" % grant, grant.code) def test_expires_can_be_none(self): grant = Grant(code="test_code") self.assertIsNone(grant.expires) self.assertTrue(grant.is_expired()) class TestAccessTokenModel(TestCase): def setUp(self): self.user = UserModel.objects.create_user("test_user", "test@example.com", "123456") def test_str(self): access_token = AccessToken(token="test_token") self.assertEqual("%s" % access_token, access_token.token) def test_user_can_be_none(self): app = Application.objects.create( name="test_app", redirect_uris="http://localhost http://example.com http://example.org", user=self.user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) access_token = AccessToken.objects.create(token="test_token", application=app, expires=timezone.now()) self.assertIsNone(access_token.user) def test_expires_can_be_none(self): access_token = AccessToken(token="test_token") self.assertIsNone(access_token.expires) self.assertTrue(access_token.is_expired()) class TestRefreshTokenModel(TestCase): def test_str(self): refresh_token = RefreshToken(token="test_token") self.assertEqual("%s" % refresh_token, refresh_token.token) django-oauth-toolkit-1.0.0/tests/test_oauth2_backends.py000066400000000000000000000074031311555211500233650ustar00rootroot00000000000000import json try: from unittest import mock except ImportError: import mock from django.test import RequestFactory, TestCase from oauth2_provider.backends import get_oauthlib_core from oauth2_provider.oauth2_backends import JSONOAuthLibCore, OAuthLibCore class TestOAuthLibCoreBackend(TestCase): def setUp(self): self.factory = RequestFactory() self.oauthlib_core = OAuthLibCore() def test_swappable_server_class(self): with mock.patch("oauth2_provider.oauth2_backends.oauth2_settings.OAUTH2_SERVER_CLASS"): oauthlib_core = OAuthLibCore() self.assertTrue(isinstance(oauthlib_core.server, mock.MagicMock)) def test_form_urlencoded_extract_params(self): payload = "grant_type=password&username=john&password=123456" request = self.factory.post("/o/token/", payload, content_type="application/x-www-form-urlencoded") uri, http_method, body, headers = self.oauthlib_core._extract_params(request) self.assertIn("grant_type=password", body) self.assertIn("username=john", body) self.assertIn("password=123456", body) def test_application_json_extract_params(self): payload = json.dumps({ "grant_type": "password", "username": "john", "password": "123456", }) request = self.factory.post("/o/token/", payload, content_type="application/json") uri, http_method, body, headers = self.oauthlib_core._extract_params(request) self.assertNotIn("grant_type=password", body) self.assertNotIn("username=john", body) self.assertNotIn("password=123456", body) class TestCustomOAuthLibCoreBackend(TestCase): """ Tests that the public API behaves as expected when we override the OAuthLibCoreBackend core methods. """ class MyOAuthLibCore(OAuthLibCore): def _get_extra_credentials(self, request): return 1 def setUp(self): self.factory = RequestFactory() def test_create_token_response_gets_extra_credentials(self): """ Make sures that extra_credentials parameter is passed to oauthlib """ payload = "grant_type=password&username=john&password=123456" request = self.factory.post("/o/token/", payload, content_type="application/x-www-form-urlencoded") with mock.patch("oauthlib.oauth2.Server.create_token_response") as create_token_response: mocked = mock.MagicMock() create_token_response.return_value = mocked, mocked, mocked core = self.MyOAuthLibCore() core.create_token_response(request) self.assertTrue(create_token_response.call_args[0][4] == 1) class TestJSONOAuthLibCoreBackend(TestCase): def setUp(self): self.factory = RequestFactory() self.oauthlib_core = JSONOAuthLibCore() def test_application_json_extract_params(self): payload = json.dumps({ "grant_type": "password", "username": "john", "password": "123456", }) request = self.factory.post("/o/token/", payload, content_type="application/json") uri, http_method, body, headers = self.oauthlib_core._extract_params(request) self.assertIn("grant_type=password", body) self.assertIn("username=john", body) self.assertIn("password=123456", body) class TestOAuthLibCore(TestCase): def setUp(self): self.factory = RequestFactory() def test_validate_authorization_request_unsafe_query(self): auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + "a_casual_token", } request = self.factory.get("/fake-resource?next=/fake", **auth_headers) oauthlib_core = get_oauthlib_core() oauthlib_core.verify_request(request, scopes=[]) django-oauth-toolkit-1.0.0/tests/test_oauth2_validators.py000066400000000000000000000237151311555211500237670ustar00rootroot00000000000000import datetime try: from unittest import mock except ImportError: import mock from django.contrib.auth import get_user_model from django.test import TransactionTestCase from django.utils import timezone from oauthlib.common import Request from oauth2_provider.exceptions import FatalClientError from oauth2_provider.models import ( get_access_token_model, get_application_model, get_refresh_token_model, ) from oauth2_provider.oauth2_validators import OAuth2Validator UserModel = get_user_model() Application = get_application_model() AccessToken = get_access_token_model() RefreshToken = get_refresh_token_model() class TestOAuth2Validator(TransactionTestCase): def setUp(self): self.user = UserModel.objects.create_user("user", "test@example.com", "123456") self.request = mock.MagicMock(wraps=Request) self.request.user = self.user self.request.grant_type = "not client" self.validator = OAuth2Validator() self.application = Application.objects.create( client_id="client_id", client_secret="client_secret", user=self.user, client_type=Application.CLIENT_PUBLIC, authorization_grant_type=Application.GRANT_PASSWORD) self.request.client = self.application def tearDown(self): self.application.delete() def test_authenticate_request_body(self): self.request.client_id = "client_id" self.request.client_secret = "" self.assertFalse(self.validator._authenticate_request_body(self.request)) self.request.client_secret = "wrong_client_secret" self.assertFalse(self.validator._authenticate_request_body(self.request)) self.request.client_secret = "client_secret" self.assertTrue(self.validator._authenticate_request_body(self.request)) def test_extract_basic_auth(self): self.request.headers = {"HTTP_AUTHORIZATION": "Basic 123456"} self.assertEqual(self.validator._extract_basic_auth(self.request), "123456") self.request.headers = {} self.assertIsNone(self.validator._extract_basic_auth(self.request)) self.request.headers = {"HTTP_AUTHORIZATION": "Dummy 123456"} self.assertIsNone(self.validator._extract_basic_auth(self.request)) self.request.headers = {"HTTP_AUTHORIZATION": "Basic"} self.assertIsNone(self.validator._extract_basic_auth(self.request)) self.request.headers = {"HTTP_AUTHORIZATION": "Basic 123456 789"} self.assertEqual(self.validator._extract_basic_auth(self.request), "123456 789") def test_authenticate_basic_auth(self): self.request.encoding = "utf-8" # client_id:client_secret self.request.headers = {"HTTP_AUTHORIZATION": "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=\n"} self.assertTrue(self.validator._authenticate_basic_auth(self.request)) def test_authenticate_basic_auth_default_encoding(self): self.request.encoding = None # client_id:client_secret self.request.headers = {"HTTP_AUTHORIZATION": "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=\n"} self.assertTrue(self.validator._authenticate_basic_auth(self.request)) def test_authenticate_basic_auth_wrong_client_id(self): self.request.encoding = "utf-8" # wrong_id:client_secret self.request.headers = {"HTTP_AUTHORIZATION": "Basic d3JvbmdfaWQ6Y2xpZW50X3NlY3JldA==\n"} self.assertFalse(self.validator._authenticate_basic_auth(self.request)) def test_authenticate_basic_auth_wrong_client_secret(self): self.request.encoding = "utf-8" # client_id:wrong_secret self.request.headers = {"HTTP_AUTHORIZATION": "Basic Y2xpZW50X2lkOndyb25nX3NlY3JldA==\n"} self.assertFalse(self.validator._authenticate_basic_auth(self.request)) def test_authenticate_basic_auth_not_b64_auth_string(self): self.request.encoding = "utf-8" # Can"t b64decode self.request.headers = {"HTTP_AUTHORIZATION": "Basic not_base64"} self.assertFalse(self.validator._authenticate_basic_auth(self.request)) def test_authenticate_basic_auth_not_utf8(self): self.request.encoding = "utf-8" # b64decode("test") will become b"\xb5\xeb-", it can"t be decoded as utf-8 self.request.headers = {"HTTP_AUTHORIZATION": "Basic test"} self.assertFalse(self.validator._authenticate_basic_auth(self.request)) def test_authenticate_client_id(self): self.assertTrue(self.validator.authenticate_client_id("client_id", self.request)) def test_authenticate_client_id_fail(self): self.application.client_type = Application.CLIENT_CONFIDENTIAL self.application.save() self.assertFalse(self.validator.authenticate_client_id("client_id", self.request)) self.assertFalse(self.validator.authenticate_client_id("fake_client_id", self.request)) def test_client_authentication_required(self): self.request.headers = {"HTTP_AUTHORIZATION": "Basic 123456"} self.assertTrue(self.validator.client_authentication_required(self.request)) self.request.headers = {} self.request.client_id = "client_id" self.request.client_secret = "client_secret" self.assertTrue(self.validator.client_authentication_required(self.request)) self.request.client_secret = "" self.assertFalse(self.validator.client_authentication_required(self.request)) self.application.client_type = Application.CLIENT_CONFIDENTIAL self.application.save() self.request.client = "" self.assertTrue(self.validator.client_authentication_required(self.request)) def test_load_application_fails_when_request_has_no_client(self): self.assertRaises(AssertionError, self.validator.authenticate_client_id, "client_id", {}) def test_rotate_refresh_token__is_true(self): self.assertTrue(self.validator.rotate_refresh_token(mock.MagicMock())) def test_save_bearer_token__without_user__raises_fatal_client(self): token = {} with self.assertRaises(FatalClientError): self.validator.save_bearer_token(token, mock.MagicMock()) def test_save_bearer_token__with_existing_tokens__does_not_create_new_tokens(self): rotate_token_function = mock.MagicMock() rotate_token_function.return_value = False self.validator.rotate_refresh_token = rotate_token_function access_token = AccessToken.objects.create( token="123", user=self.user, expires=timezone.now() + datetime.timedelta(seconds=60), application=self.application ) refresh_token = RefreshToken.objects.create( access_token=access_token, token="abc", user=self.user, application=self.application ) self.request.refresh_token_instance = refresh_token token = { "scope": "foo bar", "refresh_token": "abc", "access_token": "123", } self.assertEqual(1, RefreshToken.objects.count()) self.assertEqual(1, AccessToken.objects.count()) self.validator.save_bearer_token(token, self.request) self.assertEqual(1, RefreshToken.objects.count()) self.assertEqual(1, AccessToken.objects.count()) def test_save_bearer_token__checks_to_rotate_tokens(self): rotate_token_function = mock.MagicMock() rotate_token_function.return_value = False self.validator.rotate_refresh_token = rotate_token_function access_token = AccessToken.objects.create( token="123", user=self.user, expires=timezone.now() + datetime.timedelta(seconds=60), application=self.application ) refresh_token = RefreshToken.objects.create( access_token=access_token, token="abc", user=self.user, application=self.application ) self.request.refresh_token_instance = refresh_token token = { "scope": "foo bar", "refresh_token": "abc", "access_token": "123", } self.validator.save_bearer_token(token, self.request) rotate_token_function.assert_called_once_with(self.request) def test_save_bearer_token__with_new_token__creates_new_tokens(self): token = { "scope": "foo bar", "refresh_token": "abc", "access_token": "123", } self.assertEqual(0, RefreshToken.objects.count()) self.assertEqual(0, AccessToken.objects.count()) self.validator.save_bearer_token(token, self.request) self.assertEqual(1, RefreshToken.objects.count()) self.assertEqual(1, AccessToken.objects.count()) def test_save_bearer_token__with_new_token_equal_to_existing_token__revokes_old_tokens(self): access_token = AccessToken.objects.create( token="123", user=self.user, expires=timezone.now() + datetime.timedelta(seconds=60), application=self.application ) refresh_token = RefreshToken.objects.create( access_token=access_token, token="abc", user=self.user, application=self.application ) self.request.refresh_token_instance = refresh_token token = { "scope": "foo bar", "refresh_token": "abc", "access_token": "123", } self.assertEqual(1, RefreshToken.objects.count()) self.assertEqual(1, AccessToken.objects.count()) self.validator.save_bearer_token(token, self.request) self.assertEqual(1, RefreshToken.objects.count()) self.assertEqual(1, AccessToken.objects.count()) def test_save_bearer_token__with_no_refresh_token__creates_new_access_token_only(self): token = { "scope": "foo bar", "access_token": "123", } self.validator.save_bearer_token(token, self.request) self.assertEqual(0, RefreshToken.objects.count()) self.assertEqual(1, AccessToken.objects.count()) django-oauth-toolkit-1.0.0/tests/test_password.py000066400000000000000000000073221311555211500221730ustar00rootroot00000000000000from __future__ import unicode_literals import json from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase from django.urls import reverse from oauth2_provider.models import get_application_model from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ProtectedResourceView from .utils import get_basic_auth_header Application = get_application_model() UserModel = get_user_model() # mocking a protected resource view class ResourceView(ProtectedResourceView): def get(self, request, *args, **kwargs): return "This is a protected resource" class BaseTest(TestCase): def setUp(self): self.factory = RequestFactory() self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") self.application = Application( name="Test Password Application", user=self.dev_user, client_type=Application.CLIENT_PUBLIC, authorization_grant_type=Application.GRANT_PASSWORD, ) self.application.save() oauth2_settings._SCOPES = ["read", "write"] oauth2_settings._DEFAULT_SCOPES = ["read", "write"] def tearDown(self): self.application.delete() self.test_user.delete() self.dev_user.delete() class TestPasswordTokenView(BaseTest): def test_get_token(self): """ Request an access token using Resource Owner Password Flow """ token_request_data = { "grant_type": "password", "username": "test_user", "password": "123456", } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["token_type"], "Bearer") self.assertEqual(content["scope"], "read write") self.assertEqual(content["expires_in"], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) def test_bad_credentials(self): """ Request an access token using Resource Owner Password Flow """ token_request_data = { "grant_type": "password", "username": "test_user", "password": "NOT_MY_PASS", } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 401) class TestPasswordProtectedResource(BaseTest): def test_password_resource_access_allowed(self): token_request_data = { "grant_type": "password", "username": "test_user", "password": "123456", } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") django-oauth-toolkit-1.0.0/tests/test_rest_framework.py000066400000000000000000000251241311555211500233630ustar00rootroot00000000000000import unittest from datetime import timedelta from django.conf.urls import include, url from django.contrib.auth import get_user_model from django.http import HttpResponse from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from oauth2_provider.models import ( get_access_token_model, get_application_model, ) from oauth2_provider.settings import oauth2_settings Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() try: from rest_framework import permissions from rest_framework.views import APIView from rest_framework.test import force_authenticate, APIRequestFactory from oauth2_provider.contrib.rest_framework import ( IsAuthenticatedOrTokenHasScope, OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope ) class MockView(APIView): permission_classes = (permissions.IsAuthenticated,) def get(self, request): return HttpResponse({"a": 1, "b": 2, "c": 3}) def post(self, request): return HttpResponse({"a": 1, "b": 2, "c": 3}) class OAuth2View(MockView): authentication_classes = [OAuth2Authentication] class ScopedView(OAuth2View): permission_classes = [permissions.IsAuthenticated, TokenHasScope] required_scopes = ["scope1"] class AuthenticatedOrScopedView(OAuth2View): permission_classes = [IsAuthenticatedOrTokenHasScope] required_scopes = ["scope1"] class ReadWriteScopedView(OAuth2View): permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope] class ResourceScopedView(OAuth2View): permission_classes = [permissions.IsAuthenticated, TokenHasResourceScope] required_scopes = ["resource1"] urlpatterns = [ url(r"^oauth2/", include("oauth2_provider.urls")), url(r"^oauth2-test/$", OAuth2View.as_view()), url(r"^oauth2-scoped-test/$", ScopedView.as_view()), url(r"^oauth2-read-write-test/$", ReadWriteScopedView.as_view()), url(r"^oauth2-resource-scoped-test/$", ResourceScopedView.as_view()), url(r"^oauth2-authenticated-or-scoped-test/$", AuthenticatedOrScopedView.as_view()), ] rest_framework_installed = True except ImportError: rest_framework_installed = False @override_settings(ROOT_URLCONF=__name__) class TestOAuth2Authentication(TestCase): def setUp(self): oauth2_settings._SCOPES = ["read", "write", "scope1", "scope2", "resource1"] self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") self.application = Application.objects.create( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.dev_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.access_token = AccessToken.objects.create( user=self.test_user, scope="read write", expires=timezone.now() + timedelta(seconds=300), token="secret-access-token-key", application=self.application ) def tearDown(self): oauth2_settings._SCOPES = ["read", "write"] def _create_authorization_header(self, token): return "Bearer {0}".format(token) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_authentication_allow(self): auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_authentication_denied(self): auth = self._create_authorization_header("fake-token") response = self.client.get("/oauth2-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_authentication_or_scope_denied(self): # user is not authenticated # not a correct token auth = self._create_authorization_header("fake-token") response = self.client.get("/oauth2-authenticated-or-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) # token doesn"t have correct scope auth = self._create_authorization_header(self.access_token.token) factory = APIRequestFactory() request = factory.get("/oauth2-authenticated-or-scoped-test/") request.auth = auth force_authenticate(request, token=self.access_token) response = AuthenticatedOrScopedView.as_view()(request) # authenticated but wrong scope, this is 403, not 401 self.assertEqual(response.status_code, 403) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_scoped_permission_allow(self): self.access_token.scope = "scope1" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_authenticated_or_scoped_permission_allow(self): self.access_token.scope = "scope1" self.access_token.save() # correct token and correct scope auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-authenticated-or-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) auth = self._create_authorization_header("fake-token") # incorrect token but authenticated factory = APIRequestFactory() request = factory.get("/oauth2-authenticated-or-scoped-test/") request.auth = auth force_authenticate(request, self.test_user) response = AuthenticatedOrScopedView.as_view()(request) self.assertEqual(response.status_code, 200) # correct token but not authenticated request = factory.get("/oauth2-authenticated-or-scoped-test/") request.auth = auth self.access_token.scope = "scope1" self.access_token.save() force_authenticate(request, token=self.access_token) response = AuthenticatedOrScopedView.as_view()(request) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_scoped_permission_deny(self): self.access_token.scope = "scope2" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 403) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_read_write_permission_get_allow(self): self.access_token.scope = "read" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_read_write_permission_post_allow(self): self.access_token.scope = "write" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.post("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_read_write_permission_get_deny(self): self.access_token.scope = "write" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 403) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_read_write_permission_post_deny(self): self.access_token.scope = "read" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.post("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 403) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_resource_scoped_permission_get_allow(self): self.access_token.scope = "resource1:read" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_resource_scoped_permission_post_allow(self): self.access_token.scope = "resource1:write" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.post("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_resource_scoped_permission_get_denied(self): self.access_token.scope = "resource1:write" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 403) @unittest.skipUnless(rest_framework_installed, "djangorestframework not installed") def test_resource_scoped_permission_post_denied(self): self.access_token.scope = "resource1:read" self.access_token.save() auth = self._create_authorization_header(self.access_token.token) response = self.client.post("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 403) django-oauth-toolkit-1.0.0/tests/test_scopes.py000066400000000000000000000365641311555211500216370ustar00rootroot00000000000000from __future__ import unicode_literals import json from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory, TestCase from django.urls import reverse from oauth2_provider.compat import parse_qs, urlparse from oauth2_provider.models import ( get_access_token_model, get_application_model, get_grant_model, ) from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ReadWriteScopedResourceView, ScopedProtectedResourceView from .utils import get_basic_auth_header Application = get_application_model() AccessToken = get_access_token_model() Grant = get_grant_model() UserModel = get_user_model() # mocking a protected resource view class ScopeResourceView(ScopedProtectedResourceView): required_scopes = ["scope1"] def get(self, request, *args, **kwargs): return "This is a protected resource" class MultiScopeResourceView(ScopedProtectedResourceView): required_scopes = ["scope1", "scope2"] def get(self, request, *args, **kwargs): return "This is a protected resource" class ReadWriteResourceView(ReadWriteScopedResourceView): def get(self, request, *args, **kwargs): return "This is a read protected resource" def post(self, request, *args, **kwargs): return "This is a write protected resource" class BaseTest(TestCase): def setUp(self): self.factory = RequestFactory() self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") self.application = Application( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.dev_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.application.save() oauth2_settings._SCOPES = ["read", "write", "scope1", "scope2", "scope3"] oauth2_settings.READ_SCOPE = "read" oauth2_settings.WRITE_SCOPE = "write" def tearDown(self): oauth2_settings._SCOPES = ["read", "write"] self.application.delete() self.test_user.delete() self.dev_user.delete() class TestScopesSave(BaseTest): def test_scopes_saved_in_grant(self): """ Test scopes are properly saved in grant """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "scope1 scope2", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() grant = Grant.objects.get(code=authorization_code) self.assertEqual(grant.scope, "scope1 scope2") def test_scopes_save_in_access_token(self): """ Test scopes are properly saved in access token """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "scope1 scope2", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] at = AccessToken.objects.get(token=access_token) self.assertEqual(at.scope, "scope1 scope2") class TestScopesProtection(BaseTest): def test_scopes_protection_valid(self): """ Test access to a scope protected resource with correct scopes provided """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "scope1 scope2", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ScopeResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") def test_scopes_protection_fail(self): """ Test access to a scope protected resource with wrong scopes provided """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "scope2", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ScopeResourceView.as_view() response = view(request) self.assertEqual(response.status_code, 403) def test_multi_scope_fail(self): """ Test access to a multi-scope protected resource with wrong scopes provided """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "scope1 scope3", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = MultiScopeResourceView.as_view() response = view(request) self.assertEqual(response.status_code, 403) def test_multi_scope_valid(self): """ Test access to a multi-scope protected resource with correct scopes provided """ self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": "scope1 scope2", "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) access_token = content["access_token"] # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = MultiScopeResourceView.as_view() response = view(request) self.assertEqual(response, "This is a protected resource") class TestReadWriteScope(BaseTest): def get_access_token(self, scopes): self.client.login(username="test_user", password="123456") # retrieve a valid authorization code authcode_data = { "client_id": self.application.client_id, "state": "random_state_string", "scope": scopes, "redirect_uri": "http://example.org", "response_type": "code", "allow": True, } response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data) query_dict = parse_qs(urlparse(response["Location"]).query) authorization_code = query_dict["code"].pop() # exchange authorization code for a valid access token token_request_data = { "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": "http://example.org" } auth_headers = get_basic_auth_header(self.application.client_id, self.application.client_secret) response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) content = json.loads(response.content.decode("utf-8")) return content["access_token"] def test_improperly_configured(self): oauth2_settings.SCOPES = {"scope1": "Scope 1"} request = self.factory.get("/fake") view = ReadWriteResourceView.as_view() self.assertRaises(ImproperlyConfigured, view, request) oauth2_settings.SCOPES = {"read": "Read Scope", "write": "Write Scope"} oauth2_settings.READ_SCOPE = "ciccia" view = ReadWriteResourceView.as_view() self.assertRaises(ImproperlyConfigured, view, request) def test_properly_configured(self): oauth2_settings.SCOPES = {"scope1": "Scope 1"} request = self.factory.get("/fake") view = ReadWriteResourceView.as_view() self.assertRaises(ImproperlyConfigured, view, request) oauth2_settings.SCOPES = {"read": "Read Scope", "write": "Write Scope"} oauth2_settings.READ_SCOPE = "ciccia" view = ReadWriteResourceView.as_view() self.assertRaises(ImproperlyConfigured, view, request) def test_has_read_scope(self): access_token = self.get_access_token("read") # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ReadWriteResourceView.as_view() response = view(request) self.assertEqual(response, "This is a read protected resource") def test_no_read_scope(self): access_token = self.get_access_token("scope1") # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.get("/fake-resource", **auth_headers) request.user = self.test_user view = ReadWriteResourceView.as_view() response = view(request) self.assertEqual(response.status_code, 403) def test_has_write_scope(self): access_token = self.get_access_token("write") # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.post("/fake-resource", **auth_headers) request.user = self.test_user view = ReadWriteResourceView.as_view() response = view(request) self.assertEqual(response, "This is a write protected resource") def test_no_write_scope(self): access_token = self.get_access_token("scope1") # use token to access the resource auth_headers = { "HTTP_AUTHORIZATION": "Bearer " + access_token, } request = self.factory.post("/fake-resource", **auth_headers) request.user = self.test_user view = ReadWriteResourceView.as_view() response = view(request) self.assertEqual(response.status_code, 403) django-oauth-toolkit-1.0.0/tests/test_scopes_backend.py000066400000000000000000000006341311555211500232730ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals from oauth2_provider.scopes import SettingsScopes def test_settings_scopes_get_available_scopes(): scopes = SettingsScopes() assert scopes.get_available_scopes() == ["read", "write"] def test_settings_scopes_get_default_scopes(): scopes = SettingsScopes() assert scopes.get_default_scopes() == ["read", "write"] django-oauth-toolkit-1.0.0/tests/test_token_revocation.py000066400000000000000000000155041311555211500237030ustar00rootroot00000000000000from __future__ import unicode_literals import datetime from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase from django.urls import reverse from django.utils import timezone from oauth2_provider.compat import urlencode from oauth2_provider.models import ( get_access_token_model, get_application_model, get_refresh_token_model, ) from oauth2_provider.settings import oauth2_settings Application = get_application_model() AccessToken = get_access_token_model() RefreshToken = get_refresh_token_model() UserModel = get_user_model() class BaseTest(TestCase): def setUp(self): self.factory = RequestFactory() self.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") self.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") self.application = Application( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.dev_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.application.save() oauth2_settings._SCOPES = ["read", "write"] def tearDown(self): self.application.delete() self.test_user.delete() self.dev_user.delete() class TestRevocationView(BaseTest): def test_revoke_access_token(self): """ """ tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) query_string = urlencode({ "client_id": self.application.client_id, "client_secret": self.application.client_secret, "token": tok.token, }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string) response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"") self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) def test_revoke_access_token_public(self): public_app = Application( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.dev_user, client_type=Application.CLIENT_PUBLIC, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) public_app.save() tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=public_app, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) query_string = urlencode({ "client_id": public_app.client_id, "token": tok.token, }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string) response = self.client.post(url) self.assertEqual(response.status_code, 200) def test_revoke_access_token_with_hint(self): """ """ tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) query_string = urlencode({ "client_id": self.application.client_id, "client_secret": self.application.client_secret, "token": tok.token, "token_type_hint": "access_token" }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string) response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) def test_revoke_access_token_with_invalid_hint(self): tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) # invalid hint should have no effect query_string = urlencode({ "client_id": self.application.client_id, "client_secret": self.application.client_secret, "token": tok.token, "token_type_hint": "bad_hint" }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string) response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) def test_revoke_refresh_token(self): tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) rtok = RefreshToken.objects.create( user=self.test_user, token="999999999", application=self.application, access_token=tok ) query_string = urlencode({ "client_id": self.application.client_id, "client_secret": self.application.client_secret, "token": rtok.token, }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string) response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertFalse(RefreshToken.objects.filter(id=rtok.id).exists()) self.assertFalse(AccessToken.objects.filter(id=rtok.access_token.id).exists()) def test_revoke_token_with_wrong_hint(self): """ From the revocation rfc, `Section 4.1.2`_ : If the server is unable to locate the token using the given hint, it MUST extend its search across all of its supported token types .. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2 """ tok = AccessToken.objects.create( user=self.test_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) query_string = urlencode({ "client_id": self.application.client_id, "client_secret": self.application.client_secret, "token": tok.token, "token_type_hint": "refresh_token" }) url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string) response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) django-oauth-toolkit-1.0.0/tests/test_token_view.py000066400000000000000000000175041311555211500225060ustar00rootroot00000000000000from __future__ import unicode_literals import datetime from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from django.utils import timezone from oauth2_provider.models import ( get_access_token_model, get_application_model, ) Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() class TestAuthorizedTokenViews(TestCase): """ TestCase superclass for Authorized Token Views" Test Cases """ def setUp(self): self.foo_user = UserModel.objects.create_user("foo_user", "test@example.com", "123456") self.bar_user = UserModel.objects.create_user("bar_user", "dev@example.com", "123456") self.application = Application( name="Test Application", redirect_uris="http://localhost http://example.com http://example.org", user=self.bar_user, client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, ) self.application.save() def tearDown(self): self.foo_user.delete() self.bar_user.delete() class TestAuthorizedTokenListView(TestAuthorizedTokenViews): """ Tests for the Authorized Token ListView """ def test_list_view_authorization_required(self): """ Test that the view redirects to login page if user is not logged-in. """ response = self.client.get(reverse("oauth2_provider:authorized-token-list")) self.assertEqual(response.status_code, 302) self.assertTrue("/accounts/login/?next=" in response["Location"]) def test_empty_list_view(self): """ Test that when you have no tokens, an appropriate message is shown """ self.client.login(username="foo_user", password="123456") response = self.client.get(reverse("oauth2_provider:authorized-token-list")) self.assertEqual(response.status_code, 200) self.assertIn(b"There are no authorized tokens yet.", response.content) def test_list_view_one_token(self): """ Test that the view shows your token """ self.client.login(username="bar_user", password="123456") AccessToken.objects.create( user=self.bar_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) response = self.client.get(reverse("oauth2_provider:authorized-token-list")) self.assertEqual(response.status_code, 200) self.assertIn(b"read", response.content) self.assertIn(b"write", response.content) self.assertNotIn(b"There are no authorized tokens yet.", response.content) def test_list_view_two_tokens(self): """ Test that the view shows your tokens """ self.client.login(username="bar_user", password="123456") AccessToken.objects.create( user=self.bar_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) AccessToken.objects.create( user=self.bar_user, token="0123456789", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) response = self.client.get(reverse("oauth2_provider:authorized-token-list")) self.assertEqual(response.status_code, 200) self.assertNotIn(b"There are no authorized tokens yet.", response.content) def test_list_view_shows_correct_user_token(self): """ Test that only currently logged-in user"s tokens are shown """ self.client.login(username="bar_user", password="123456") AccessToken.objects.create( user=self.foo_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) response = self.client.get(reverse("oauth2_provider:authorized-token-list")) self.assertEqual(response.status_code, 200) self.assertIn(b"There are no authorized tokens yet.", response.content) class TestAuthorizedTokenDeleteView(TestAuthorizedTokenViews): """ Tests for the Authorized Token DeleteView """ def test_delete_view_authorization_required(self): """ Test that the view redirects to login page if user is not logged-in. """ self.token = AccessToken.objects.create( user=self.foo_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) url = reverse("oauth2_provider:authorized-token-delete", kwargs={"pk": self.token.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 302) self.assertTrue("/accounts/login/?next=" in response["Location"]) def test_delete_view_works(self): """ Test that a GET on this view returns 200 if the token belongs to the logged-in user. """ self.token = AccessToken.objects.create( user=self.foo_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="foo_user", password="123456") url = reverse("oauth2_provider:authorized-token-delete", kwargs={"pk": self.token.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_delete_view_token_belongs_to_user(self): """ Test that a 404 is returned when trying to GET this view with someone else"s tokens. """ self.token = AccessToken.objects.create( user=self.foo_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="bar_user", password="123456") url = reverse("oauth2_provider:authorized-token-delete", kwargs={"pk": self.token.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_delete_view_post_actually_deletes(self): """ Test that a POST on this view works if the token belongs to the logged-in user. """ self.token = AccessToken.objects.create( user=self.foo_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="foo_user", password="123456") url = reverse("oauth2_provider:authorized-token-delete", kwargs={"pk": self.token.pk}) response = self.client.post(url) self.assertFalse(AccessToken.objects.exists()) self.assertRedirects(response, reverse("oauth2_provider:authorized-token-list")) def test_delete_view_only_deletes_user_own_token(self): """ Test that a 404 is returned when trying to POST on this view with someone else"s tokens. """ self.token = AccessToken.objects.create( user=self.foo_user, token="1234567890", application=self.application, expires=timezone.now() + datetime.timedelta(days=1), scope="read write" ) self.client.login(username="bar_user", password="123456") url = reverse("oauth2_provider:authorized-token-delete", kwargs={"pk": self.token.pk}) response = self.client.post(url) self.assertTrue(AccessToken.objects.exists()) self.assertEqual(response.status_code, 404) django-oauth-toolkit-1.0.0/tests/test_validators.py000066400000000000000000000031701311555211500224760ustar00rootroot00000000000000from __future__ import unicode_literals from django.core.validators import ValidationError from django.test import TestCase from oauth2_provider.settings import oauth2_settings from oauth2_provider.validators import validate_uris class TestValidators(TestCase): def test_validate_good_uris(self): good_uris = "http://example.com/ http://example.org/?key=val http://example" # Check ValidationError not thrown validate_uris(good_uris) def test_validate_custom_uri_scheme(self): oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["my-scheme", "http"] good_uris = "my-scheme://example.com http://example.com" # Check ValidationError not thrown validate_uris(good_uris) def test_validate_whitespace_separators(self): # Check that whitespace can be used as a separator good_uris = "http://example\r\nhttp://example\thttp://example" # Check ValidationError not thrown validate_uris(good_uris) def test_validate_bad_uris(self): bad_uri = "http://example.com/#fragment" self.assertRaises(ValidationError, validate_uris, bad_uri) bad_uri = "http:/example.com" self.assertRaises(ValidationError, validate_uris, bad_uri) bad_uri = "my-scheme://example.com" self.assertRaises(ValidationError, validate_uris, bad_uri) bad_uri = "sdklfsjlfjljdflksjlkfjsdkl" self.assertRaises(ValidationError, validate_uris, bad_uri) bad_uri = " " self.assertRaises(ValidationError, validate_uris, bad_uri) bad_uri = "" self.assertRaises(ValidationError, validate_uris, bad_uri) django-oauth-toolkit-1.0.0/tests/urls.py000066400000000000000000000003701311555211500202530ustar00rootroot00000000000000from django.conf.urls import include, url from django.contrib import admin admin.autodiscover() urlpatterns = [ url(r"^o/", include("oauth2_provider.urls", namespace="oauth2_provider")), ] urlpatterns += [url(r"^admin/", admin.site.urls)] django-oauth-toolkit-1.0.0/tests/utils.py000066400000000000000000000006631311555211500204330ustar00rootroot00000000000000from __future__ import unicode_literals import base64 def get_basic_auth_header(user, password): """ Return a dict containg the correct headers to set to make HTTP Basic Auth request """ user_pass = "{0}:{1}".format(user, password) auth_string = base64.b64encode(user_pass.encode("utf-8")) auth_headers = { "HTTP_AUTHORIZATION": "Basic " + auth_string.decode("utf-8"), } return auth_headers django-oauth-toolkit-1.0.0/tox.ini000066400000000000000000000017271311555211500170740ustar00rootroot00000000000000[tox] envlist = py27-django{110,111}, py35-django{110,111,master}, py36-djangomaster, docs, flake8 [testenv] commands = pytest --cov=oauth2_provider --cov-report= --cov-append setenv = DJANGO_SETTINGS_MODULE = tests.settings PYTHONPATH = {toxinidir} PYTHONWARNINGS = all deps = django110: Django >=1.10, <1.11 django111: Django >=1.11, <2.0 djangomaster: https://github.com/django/django/archive/master.tar.gz djangorestframework >=3.5 coverage pytest pytest-cov pytest-django pytest-xdist py27: mock [testenv:docs] basepython = python changedir = docs whitelist_externals = make commands = make html deps = sphinx [testenv:flake8] commands = flake8 deps = flake8 flake8-import-order flake8-quotes [coverage:run] source = oauth2_provider omit = */migrations/* [pytest] django_find_project = false [flake8] max-line-length = 110 exclude = docs/, migrations/, .tox/ import-order-style = smarkets application-import-names = oauth2_provider inline-quotes = "