├── .coveragerc ├── .gitignore ├── .pylintrc ├── .python-version ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── aiohttp.rst ├── client_authenticator.rst ├── conf.py ├── error.rst ├── examples │ ├── aiohttp_server.py │ ├── authorization_code_grant.py │ ├── base_server.py │ ├── client_credentials_grant.py │ ├── flask_server.py │ ├── implicit_grant.py │ ├── pyramid │ │ ├── README.md │ │ ├── base.py │ │ └── impl.py │ ├── resource_owner_grant.py │ ├── stateless_client_server.py │ └── tornado_server.py ├── flask.rst ├── frameworks.rst ├── grant.rst ├── index.rst ├── log.rst ├── oauth2.rst ├── store.rst ├── store │ ├── dbapi.rst │ ├── dynamodb.rst │ ├── memcache.rst │ ├── memory.rst │ ├── mongodb.rst │ ├── mysql.rst │ └── redisdb.rst ├── token_generator.rst ├── tornado.rst ├── unique_token.rst └── web.rst ├── oauth2 ├── __init__.py ├── client_authenticator.py ├── compatibility.py ├── datatype.py ├── error.py ├── grant.py ├── log.py ├── store │ ├── __init__.py │ ├── dbapi │ │ ├── __init__.py │ │ └── mysql.py │ ├── dynamodb.py │ ├── memcache.py │ ├── memory.py │ ├── mongodb.py │ ├── redisdb.py │ └── stateless.py ├── test │ ├── __init__.py │ ├── functional │ │ ├── __init__.py │ │ └── test_authorization_code.py │ ├── store │ │ ├── __init__.py │ │ ├── test_dbapi.py │ │ ├── test_memcache.py │ │ ├── test_memory.py │ │ ├── test_mongodb.py │ │ └── test_redisdb.py │ ├── test_client_authenticator.py │ ├── test_datatype.py │ ├── test_grant.py │ ├── test_oauth2.py │ ├── test_tokengenerator.py │ └── test_web.py ├── tokengenerator.py └── web │ ├── __init__.py │ ├── aiohttp.py │ ├── flask.py │ ├── tornado.py │ └── wsgi.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | raise NotImplementedError 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | 3 | # IDE 4 | .project 5 | .pydevproject 6 | .idea/ 7 | .settings/ 8 | 9 | # Python 10 | *.pyc 11 | *.egg-info/ 12 | dist/ 13 | build/ 14 | _build/ 15 | __pycache__ 16 | 17 | # coverage 18 | .coverage 19 | cover/ 20 | 21 | # Vagrant 22 | .vagrant/ 23 | Berksfile.lock 24 | cookbooks/ 25 | Gemfile.lock 26 | /GPATH 27 | /GRTAGS 28 | /GTAGS 29 | /TAGS 30 | /cscope.files 31 | /cscope.out 32 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: pip 4 | python: 5 | - 3.4 6 | - 3.5 7 | - 3.6 8 | - 3.7 9 | - 3.8 10 | - 3.9 11 | - 3.10 12 | env: 13 | - DB=mongodb 14 | - DB=mysql 15 | - DB=redis-server 16 | before_script: 17 | - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE IF NOT EXISTS testdb;'; 18 | fi" 19 | services: 20 | - mongodb 21 | - redis-server 22 | install: 23 | - pip install --upgrade setuptools 24 | - pip install -r requirements.txt 25 | - pip install -r requirements-dev.txt 26 | script: make test 27 | deploy: 28 | provider: pypi 29 | user: darkanthey 30 | password: 31 | secure: F1Sg5YXT6ZwsOvo5mfGGLwRTgyaYE/jXQMEyN9Sz223IeuJy8YQYempMAhPZbJY8zKI4FM9KIVNZB6JH9AXjV7fUS4axee8eqrSHFYqEz+gPVk6BYepYtUBw1U6xRbJeMFwNg6kaJvY+TGmyAKEeWyHPLDNEdU+tKHB4fyLnbIc= 32 | on: 33 | tags: true 34 | repo: darkanthey/oauth2-stateless 35 | condition: $TRAVIS_PYTHON_VERSION = '3.10' 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.2 2 | 3 | Features: 4 | 5 | - Add python 3.8..3.10 support ([@darkanthey][]) 6 | 7 | ## 1.1.1 8 | 9 | Features: 10 | 11 | - Remove PY2 support ([@darkanthey][]) 12 | 13 | Bugfixes: 14 | 15 | - Fix test DeprecationWarning: Please use assertRegex instead. 16 | - Add more example with user_id example. 17 | - Add requirements-dev.txt and all datastore and web adapter move there. 18 | 19 | ## 1.1.0 20 | 21 | Features: 22 | 23 | - aiohttp web framework support ([@darkanthey][]) 24 | 25 | Bugfixes: 26 | 27 | - For wider support redis version rs.setex was replaced by rs.set with ex param. 28 | 29 | ## 1.0.3 30 | 31 | Bugfixes: 32 | 33 | - All examples modified to support both PY3/PY2 versions. ([@darkanthey][]) 34 | - flask_server example was added. ([@darkanthey][]) 35 | - json throughout the project now imported from oauth2.compatibility. ([@darkanthey][]) 36 | - Support aiohttp are coming. ([@darkanthey][]) 37 | 38 | ## 1.0.2 39 | 40 | Bugfixes: 41 | 42 | - Some oauth implementations have 'Content-Type: application/json' for oauth/token. ([@darkanthey][]) 43 | 44 | ## 1.0.1 45 | 46 | Bugfixes: 47 | 48 | - Fix an exception when requesting an unknown URL on Python 3.x ([@darkanthey][]) 49 | - Add error message to response for bad redirect URIs. ([@darkanthey][]) 50 | 51 | ## 1.0.0 52 | 53 | Features: 54 | 55 | - Stateless token support ([@darkanthey][]) 56 | - Dynamodb token store ([@darkanthey][]) 57 | - Support for Python 2.7 - 3.7 ([@darkanthey][]) 58 | 59 | [@darkanthey]: https://github.com/darkanthey 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andrew Grytsenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Run All unit tests 2 | test: 3 | nosetests 4 | 5 | unittest: 6 | nosetests --exclude='functional' 7 | 8 | functest: 9 | nosetests --where=oauth2/test/functional 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oauth2-stateless 2 | 3 | Oauth2-stateless is a framework that aims at making it easy to provide authentication 4 | via [OAuth 2.0](http://tools.ietf.org/html/rfc6749) within an application stack. 5 | Main difference of this library is the simplicity 6 | and the ability to work without any database just with 'stateless' 7 | tokens based on **JWT** [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token). 8 | 9 | [Documentation](http://oauth2-stateless.readthedocs.org/en/latest/index.html) 10 | 11 | 12 | # Status 13 | 14 | [![Travis Build Status][build-badge]][build] 15 | [![License](http://img.shields.io/badge/Licence-MIT-brightgreen.svg)](LICENSE) 16 | 17 | Oauth2-stateless has reached its beta phase. All main parts of the [OAuth 2.0 RFC](http://tools.ietf.org/html/rfc6749) such as the various types of Grants, Refresh Token and Scopes have been implemented. 18 | 19 | 20 | # Installation 21 | 22 | oauth2-stateless is [available on PyPI](http://pypi.python.org/pypi/oauth2-stateless/) 23 | 24 | ``` bash 25 | pip install oauth2-stateless 26 | ``` 27 | 28 | 29 | # Usage 30 | 31 | ## Example Authorization server 32 | 33 | ``` python 34 | from wsgiref.simple_server import make_server 35 | import oauth2 36 | import oauth2.grant 37 | import oauth2.error 38 | from oauth2.store.memory import ClientStore 39 | from oauth2.store.stateless import Token Store 40 | import oauth2.tokengenerator 41 | import oauth2.web.wsgi 42 | 43 | 44 | # Create a SiteAdapter to interact with the user. 45 | # This can be used to display confirmation dialogs and the like. 46 | class ExampleSiteAdapter(oauth2.web.AuthorizationCodeGrantSiteAdapter, oauth2.web.ImplicitGrantSiteAdapter): 47 | TEMPLATE = ''' 48 | 49 | 50 |

51 | confirm 52 |

53 |

54 | deny 55 |

56 | 57 | ''' 58 | 59 | def authenticate(self, request, environ, scopes, client): 60 | # Check if the user has granted access 61 | example_user_id = 123 62 | example_ext_data = {} 63 | 64 | if request.post_param("confirm") == "confirm": 65 | return example_ext_data, example_user_id 66 | 67 | raise oauth2.error.UserNotAuthenticated 68 | 69 | def render_auth_page(self, request, response, environ, scopes, client): 70 | url = request.path + "?" + request.query_string 71 | response.body = self.TEMPLATE.format(url=url) 72 | return response 73 | 74 | def user_has_denied_access(self, request): 75 | # Check if the user has denied access 76 | if request.post_param("deny") == "deny": 77 | return True 78 | return False 79 | 80 | # Create an in-memory storage to store your client apps. 81 | client_store = ClientStore() 82 | # Add a client 83 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost/callback"]) 84 | 85 | site_adapter = ExampleSiteAdapter() 86 | 87 | # Create an in-memory storage to store issued tokens. 88 | # LocalTokenStore can store access and auth tokens 89 | stateless_token = oauth2.tokengenerator.StatelessTokenGenerator(secret_key='xxx') 90 | token_store = TokenStore(stateless) 91 | 92 | # Create the controller. 93 | provider = oauth2.Provider( 94 | access_token_store=token_store, 95 | auth_code_store=token_store, 96 | client_store=client_store, 97 | token_generator=stateless_token) 98 | ) 99 | 100 | # Add Grants you want to support 101 | provider.add_grant(oauth2.grant.AuthorizationCodeGrant(site_adapter=site_adapter)) 102 | provider.add_grant(oauth2.grant.ImplicitGrant(site_adapter=site_adapter)) 103 | 104 | # Add refresh token capability and set expiration time of access tokens to 30 days 105 | provider.add_grant(oauth2.grant.RefreshToken(expires_in=2592000)) 106 | 107 | # Wrap the controller with the Wsgi adapter 108 | app = oauth2.web.wsgi.Application(provider=provider) 109 | 110 | if __name__ == "__main__": 111 | httpd = make_server('', 8080, app) 112 | httpd.serve_forever() 113 | ``` 114 | 115 | This example only shows how to instantiate the server. 116 | It is not a working example as a client app is missing. 117 | Take a look at the [examples](docs/examples/) directory. 118 | 119 | Or just run this example: 120 | 121 | ``` bash 122 | python docs/examples/stateless_client_server.py 123 | ``` 124 | 125 | This is already a workable example. They can work without database 126 | because oauth token already contain all the necessary information like 127 | a user_id, grant_type, data, scopes and client_id. 128 | If you want to check user state like a ban, disable, etc. 129 | You can check this param on server site from database. By adding this check to 130 | /api/me or redefine oauth2.tokengenerator and add specific logic. 131 | 132 | 133 | # Supported storage backends 134 | 135 | Oauth2-stateless does not force you to use a specific database or you 136 | can work without database with stateless token. 137 | 138 | It currently supports these storage backends out-of-the-box: 139 | 140 | - MongoDB 141 | - MySQL 142 | - Redis 143 | - Memcached 144 | - Dynamodb 145 | 146 | However, you are not not bound to these implementations. 147 | By adhering to the interface defined by the base classes in **oauth2.store**, 148 | you can easily add an implementation of your backend. 149 | It also is possible to mix different backends and e.g. read data of a client 150 | from MongoDB while saving all tokens in memcached for fast access. 151 | 152 | Take a look at the examples in the [examples](docs/examples/) directory of the project. 153 | 154 | 155 | # Site adapter 156 | 157 | - aiohttp 158 | - flask 159 | - tornado 160 | - uwsgi 161 | 162 | Like for storage, oauth2-stateless does not define how you identify a 163 | user or show a confirmation dialogue. 164 | Instead your application should use the API defined by _oauth2.web.SiteAdapter_. 165 | 166 | 167 | # Contributors 168 | 169 | [DarkAnthey](https://github.com/darkanthey) | 170 | :---: 171 | |[DarkAnthey](https://github.com/darkanthey)| 172 | 173 | [build-badge]: https://travis-ci.org/darkanthey/oauth2-stateless.svg?branch=master 174 | [build]: https://travis-ci.org/darkanthey/oauth2-stateless.svg?branch=master 175 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat 176 | [license]: https://github.com/darkanthey/oauth2-stateless/blob/master/LICENSE 177 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/oauth2-stateless.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/oauth2-stateless.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/oauth2-stateless" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/oauth2-stateless" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/aiohttp.rst: -------------------------------------------------------------------------------- 1 | aiohttp 2 | ======= 3 | 4 | .. automodule:: oauth2.web.aiohttp 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/client_authenticator.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.client_authenticator`` --- Client authentication 2 | ========================================================= 3 | 4 | .. automodule:: oauth2.client_authenticator 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # oauth2-stateless documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Oct 13 20:22:10 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | import oauth2 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'oauth2-stateless' 45 | copyright = u'2018, Andrew Grytsenko' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = release = '1.0.0' 53 | 54 | # The language for content autogenerated by Sphinx. Refer to documentation 55 | # for a list of supported languages. 56 | #language = None 57 | 58 | # There are two options for replacing |today|: either, you set today to some 59 | # non-false value, then it is used: 60 | #today = '' 61 | # Else, today_fmt is used as the format for a strftime call. 62 | #today_fmt = '%B %d, %Y' 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | exclude_patterns = ['_build'] 67 | 68 | # The reST default role (used for this markup: `text`) to use for all documents. 69 | #default_role = None 70 | 71 | # If true, '()' will be appended to :func: etc. cross-reference text. 72 | #add_function_parentheses = True 73 | 74 | # If true, the current module name will be prepended to all description 75 | # unit titles (such as .. function::). 76 | #add_module_names = True 77 | 78 | # If true, sectionauthor and moduleauthor directives will be shown in the 79 | # output. They are ignored by default. 80 | #show_authors = False 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = 'sphinx' 84 | 85 | # A list of ignored prefixes for module index sorting. 86 | #modindex_common_prefix = [] 87 | 88 | # If true, keep warnings as "system message" paragraphs in the built documents. 89 | #keep_warnings = False 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'sphinx_rtd_theme' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 123 | # using the given strftime format. 124 | #html_last_updated_fmt = '%b %d, %Y' 125 | 126 | # If true, SmartyPants will be used to convert quotes and dashes to 127 | # typographically correct entities. 128 | #html_use_smartypants = True 129 | 130 | # Custom sidebar templates, maps document names to template names. 131 | #html_sidebars = {} 132 | 133 | # Additional templates that should be rendered to pages, maps page names to 134 | # template names. 135 | #html_additional_pages = {} 136 | 137 | # If false, no module index is generated. 138 | #html_domain_indices = True 139 | 140 | # If false, no index is generated. 141 | #html_use_index = True 142 | 143 | # If true, the index is split into individual pages for each letter. 144 | #html_split_index = False 145 | 146 | # If true, links to the reST sources are added to the pages. 147 | #html_show_sourcelink = True 148 | 149 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 150 | #html_show_sphinx = True 151 | 152 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 153 | #html_show_copyright = True 154 | 155 | # If true, an OpenSearch description file will be output, and all pages will 156 | # contain a tag referring to it. The value of this option must be the 157 | # base URL from which the finished HTML is served. 158 | #html_use_opensearch = '' 159 | 160 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 161 | #html_file_suffix = None 162 | 163 | # Output file base name for HTML help builder. 164 | htmlhelp_basename = 'oauth2-statelessdoc' 165 | 166 | 167 | # -- Options for LaTeX output -------------------------------------------------- 168 | 169 | latex_elements = { 170 | # The paper size ('letterpaper' or 'a4paper'). 171 | #'papersize': 'letterpaper', 172 | 173 | # The font size ('10pt', '11pt' or '12pt'). 174 | #'pointsize': '10pt', 175 | 176 | # Additional stuff for the LaTeX preamble. 177 | #'preamble': '', 178 | } 179 | 180 | # Grouping the document tree into LaTeX files. List of tuples 181 | # (source start file, target name, title, author, documentclass [howto/manual]). 182 | latex_documents = [ 183 | ('index', 'oauth2-sateless.tex', u'oauth2-stateless Documentation', 184 | u'Andrew Grytsenko', 'manual'), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | #latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | #latex_use_parts = False 194 | 195 | # If true, show page references after internal links. 196 | #latex_show_pagerefs = False 197 | 198 | # If true, show URL addresses after external links. 199 | #latex_show_urls = False 200 | 201 | # Documents to append as an appendix to all manuals. 202 | #latex_appendices = [] 203 | 204 | # If false, no module index is generated. 205 | #latex_domain_indices = True 206 | 207 | 208 | # -- Options for manual page output -------------------------------------------- 209 | 210 | # One entry per manual page. List of tuples 211 | # (source start file, name, description, authors, manual section). 212 | man_pages = [ 213 | ('index', 'oauth2-stateless', u'oauth2-sateless Documentation', 214 | [u'Andrew Grytsenko'], 1) 215 | ] 216 | 217 | # If true, show URL addresses after external links. 218 | #man_show_urls = False 219 | 220 | 221 | # -- Options for Texinfo output ------------------------------------------------ 222 | 223 | # Grouping the document tree into Texinfo files. List of tuples 224 | # (source start file, target name, title, author, 225 | # dir menu entry, description, category) 226 | texinfo_documents = [ 227 | ('index', 'oauth2-stateless', u'oauth2-stateless Documentation', 228 | u'Andrew Grytsenko', 'oauth2-stateless', 'One line description of project.', 'Miscellaneous'), 229 | ] 230 | 231 | # Documents to append as an appendix to all manuals. 232 | #texinfo_appendices = [] 233 | 234 | # If false, no module index is generated. 235 | #texinfo_domain_indices = True 236 | 237 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 238 | #texinfo_show_urls = 'footnote' 239 | 240 | # If true, do not generate a @detailmenu in the "Top" node's menu. 241 | #texinfo_no_detailmenu = False 242 | -------------------------------------------------------------------------------- /docs/error.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.error`` --- Error classes 2 | ================================== 3 | 4 | .. automodule:: oauth2.error 5 | 6 | .. autoclass:: AccessTokenNotFound 7 | 8 | .. autoclass:: AuthCodeNotFound 9 | 10 | .. autoclass:: ClientNotFoundError 11 | 12 | .. autoclass:: OAuthBaseError 13 | 14 | .. autoclass:: OAuthInvalidError 15 | 16 | .. autoclass:: UserNotAuthenticated 17 | -------------------------------------------------------------------------------- /docs/examples/aiohttp_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import WSGIRequestHandler, make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | if sys.version_info >= (3, 5, 3): 10 | from multiprocessing import Process 11 | from urllib.request import urlopen 12 | else: 13 | sys.stderr.write("You need python 3.5.3+ to run this script\n") 14 | exit(1) 15 | 16 | import aiohttp.web 17 | from oauth2 import Provider 18 | from oauth2.compatibility import json, parse_qs, urlencode 19 | from oauth2.error import UserNotAuthenticated 20 | from oauth2.grant import AuthorizationCodeGrant 21 | from oauth2.store.memory import ClientStore, TokenStore 22 | from oauth2.tokengenerator import Uuid4TokenGenerator 23 | from oauth2.web import AuthorizationCodeGrantSiteAdapter 24 | from oauth2.web.aiohttp import OAuth2Handler 25 | 26 | logging.basicConfig(level=logging.DEBUG) 27 | 28 | 29 | class ClientRequestHandler(WSGIRequestHandler): 30 | """ 31 | Request handler that enables formatting of the log messages on the console. 32 | 33 | This handler is used by the client application. 34 | """ 35 | def address_string(self): 36 | return "client app" 37 | 38 | 39 | class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter): 40 | """ 41 | This adapter renders a confirmation page so the user can confirm the auth 42 | request. 43 | """ 44 | 45 | CONFIRMATION_TEMPLATE = """ 46 | 47 | 48 |

49 | confirm 50 |

51 |

52 | deny 53 |

54 | 55 | 56 | """ 57 | 58 | def render_auth_page(self, request, response, environ, scopes, client): 59 | page_url = request.path + "?" + request.query_string 60 | response.body = self.CONFIRMATION_TEMPLATE.format(url=page_url) 61 | 62 | return response 63 | 64 | def authenticate(self, request, environ, scopes, client): 65 | example_user_id = 123 66 | example_ext_data = {} 67 | if request.method == "GET": 68 | if request.get_param("confirm") == "1": 69 | return example_ext_data, example_user_id 70 | raise UserNotAuthenticated 71 | 72 | def user_has_denied_access(self, request): 73 | if request.method == "GET": 74 | if request.get_param("confirm") == "0": 75 | return True 76 | return False 77 | 78 | 79 | class ClientApplication(object): 80 | """ 81 | Very basic application that simulates calls to the API of the 82 | oauth2-stateless app. 83 | """ 84 | callback_url = "http://localhost:8080/callback" 85 | client_id = "abc" 86 | client_secret = "xyz" 87 | api_server_url = "http://localhost:8081" 88 | 89 | def __init__(self): 90 | self.access_token_result = None 91 | self.access_token = None 92 | self.auth_token = None 93 | self.token_type = "" 94 | 95 | def __call__(self, env, start_response): 96 | if env["PATH_INFO"] == "/app": 97 | status, body, headers = self._serve_application(env) 98 | elif env["PATH_INFO"] == "/callback": 99 | status, body, headers = self._read_auth_token(env) 100 | else: 101 | status = "301 Moved" 102 | body = "" 103 | headers = {"Location": "/app"} 104 | 105 | start_response(status, [(header, val) for header, val in headers.items()]) 106 | return [body.encode('utf-8')] 107 | 108 | def _request_access_token(self): 109 | print("Requesting access token...") 110 | 111 | post_params = {"client_id": self.client_id, 112 | "client_secret": self.client_secret, 113 | "code": self.auth_token, 114 | "grant_type": "authorization_code", 115 | "redirect_uri": self.callback_url} 116 | token_endpoint = self.api_server_url + "/token" 117 | 118 | token_result = urlopen(token_endpoint, urlencode(post_params).encode('utf-8')) 119 | result = json.loads(token_result.read().decode('utf-8')) 120 | 121 | self.access_token_result = result 122 | self.access_token = result["access_token"] 123 | self.token_type = result["token_type"] 124 | 125 | confirmation = "Received access token '%s' of type '%s'" % (self.access_token, self.token_type) 126 | print(confirmation) 127 | return "302 Found", "", {"Location": "/app"} 128 | 129 | def _read_auth_token(self, env): 130 | print("Receiving authorization token...") 131 | 132 | query_params = parse_qs(env["QUERY_STRING"]) 133 | 134 | if "error" in query_params: 135 | location = "/app?error=" + query_params["error"][0] 136 | return "302 Found", "", {"Location": location} 137 | 138 | self.auth_token = query_params["code"][0] 139 | 140 | print("Received temporary authorization token '%s'" % (self.auth_token,)) 141 | return "302 Found", "", {"Location": "/app"} 142 | 143 | def _request_auth_token(self): 144 | print("Requesting authorization token...") 145 | 146 | auth_endpoint = self.api_server_url + "/authorize" 147 | query = urlencode({"client_id": "abc", 148 | "redirect_uri": self.callback_url, 149 | "response_type": "code"}) 150 | 151 | location = "%s?%s" % (auth_endpoint, query) 152 | return "302 Found", "", {"Location": location} 153 | 154 | def _serve_application(self, env): 155 | query_params = parse_qs(env["QUERY_STRING"]) 156 | if ("error" in query_params and query_params["error"][0] == "access_denied"): 157 | return "200 OK", "User has denied access", {} 158 | 159 | if self.access_token_result is None: 160 | if self.auth_token is None: 161 | return self._request_auth_token() 162 | return self._request_access_token() 163 | confirmation = "Current access token '%s' of type '%s'" % (self.access_token, self.token_type) 164 | return "200 OK", str(confirmation), {} 165 | 166 | 167 | def run_app_server(): 168 | app = ClientApplication() 169 | 170 | try: 171 | httpd = make_server('', 8080, app, handler_class=ClientRequestHandler) 172 | 173 | print("Starting Client app on http://localhost:8080/...") 174 | httpd.serve_forever() 175 | except KeyboardInterrupt: 176 | httpd.server_close() 177 | 178 | 179 | def run_auth_server(): 180 | client_store = ClientStore() 181 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost:8080/callback"]) 182 | 183 | token_store = TokenStore() 184 | 185 | provider = Provider(access_token_store=token_store, 186 | auth_code_store=token_store, client_store=client_store, 187 | token_generator=Uuid4TokenGenerator()) 188 | provider.add_grant(AuthorizationCodeGrant(site_adapter=TestSiteAdapter())) 189 | 190 | try: 191 | app = aiohttp.web.Application() 192 | 193 | handler = OAuth2Handler(provider) 194 | 195 | app.router.add_get(provider.authorize_path, handler.dispatch_request) 196 | app.router.add_post(provider.authorize_path, handler.post_dispatch_request) 197 | app.router.add_post(provider.token_path, handler.post_dispatch_request) 198 | 199 | aiohttp.web.run_app(app, host='127.0.0.1', port=8081) 200 | print("Starting OAuth2 server on http://localhost:8081/...") 201 | except KeyboardInterrupt: 202 | aiohttp.web.close() 203 | 204 | def main(): 205 | auth_server = Process(target=run_auth_server) 206 | auth_server.start() 207 | app_server = Process(target=run_app_server) 208 | app_server.start() 209 | print("Access http://localhost:8080/app in your browser") 210 | 211 | def sigint_handler(signal, frame): 212 | print("Terminating servers...") 213 | auth_server.terminate() 214 | auth_server.join() 215 | app_server.terminate() 216 | app_server.join() 217 | 218 | signal.signal(signal.SIGINT, sigint_handler) 219 | 220 | if __name__ == "__main__": 221 | main() 222 | -------------------------------------------------------------------------------- /docs/examples/authorization_code_grant.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import WSGIRequestHandler, make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | from oauth2 import Provider 10 | from oauth2.compatibility import json, parse_qs, urlencode 11 | from oauth2.error import UserNotAuthenticated 12 | from oauth2.grant import AuthorizationCodeGrant 13 | from oauth2.store.memory import ClientStore, TokenStore 14 | from oauth2.tokengenerator import Uuid4TokenGenerator 15 | from oauth2.web import AuthorizationCodeGrantSiteAdapter 16 | from oauth2.web.wsgi import Application 17 | 18 | from multiprocessing import Process 19 | from urllib.request import urlopen 20 | 21 | logging.basicConfig(level=logging.DEBUG) 22 | 23 | 24 | class ClientRequestHandler(WSGIRequestHandler): 25 | """ 26 | Request handler that enables formatting of the log messages on the console. 27 | 28 | This handler is used by the client application. 29 | """ 30 | def address_string(self): 31 | return "client app" 32 | 33 | 34 | class OAuthRequestHandler(WSGIRequestHandler): 35 | """ 36 | Request handler that enables formatting of the log messages on the console. 37 | 38 | This handler is used by the oauth2-stateless application. 39 | """ 40 | def address_string(self): 41 | return "oauth2-stateless" 42 | 43 | 44 | class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter): 45 | """ 46 | This adapter renders a confirmation page so the user can confirm the auth 47 | request. 48 | """ 49 | 50 | CONFIRMATION_TEMPLATE = """ 51 | 52 | 53 |

54 | confirm 55 |

56 |

57 | deny 58 |

59 | 60 | 61 | """ 62 | 63 | def render_auth_page(self, request, response, environ, scopes, client): 64 | url = request.path + "?" + request.query_string 65 | response.body = self.CONFIRMATION_TEMPLATE.format(url=url) 66 | 67 | return response 68 | 69 | def authenticate(self, request, environ, scopes, client): 70 | example_user_id = 123 71 | example_ext_data = {} 72 | if request.method == "GET": 73 | if request.get_param("confirm") == "1": 74 | return example_ext_data, example_user_id 75 | raise UserNotAuthenticated 76 | 77 | def user_has_denied_access(self, request): 78 | if request.method == "GET": 79 | if request.get_param("confirm") == "0": 80 | return True 81 | return False 82 | 83 | 84 | class ClientApplication(object): 85 | """ 86 | Very basic application that simulates calls to the API of the 87 | oauth2-stateless app. 88 | """ 89 | callback_url = "http://localhost:8080/callback" 90 | client_id = "abc" 91 | client_secret = "xyz" 92 | api_server_url = "http://localhost:8081" 93 | 94 | def __init__(self): 95 | self.access_token_result = None 96 | self.access_token = None 97 | self.auth_token = None 98 | self.token_type = "" 99 | 100 | def __call__(self, env, start_response): 101 | if env["PATH_INFO"] == "/app": 102 | status, body, headers = self._serve_application(env) 103 | elif env["PATH_INFO"] == "/callback": 104 | status, body, headers = self._read_auth_token(env) 105 | else: 106 | status = "301 Moved" 107 | body = "" 108 | headers = {"Location": "/app"} 109 | 110 | start_response(status, [(header, val) for header, val in headers.items()]) 111 | return [body.encode('utf-8')] 112 | 113 | def _request_access_token(self): 114 | print("Requesting access token...") 115 | 116 | post_params = {"client_id": self.client_id, 117 | "client_secret": self.client_secret, 118 | "code": self.auth_token, 119 | "grant_type": "authorization_code", 120 | "redirect_uri": self.callback_url} 121 | token_endpoint = self.api_server_url + "/token" 122 | 123 | token_result = urlopen(token_endpoint, urlencode(post_params).encode('utf-8')) 124 | result = json.loads(token_result.read().decode('utf-8')) 125 | 126 | self.access_token_result = result 127 | self.access_token = result["access_token"] 128 | self.token_type = result["token_type"] 129 | 130 | confirmation = "Received access token '%s' of type '%s'" % (self.access_token, self.token_type) 131 | print(confirmation) 132 | return "302 Found", "", {"Location": "/app"} 133 | 134 | def _read_auth_token(self, env): 135 | print("Receiving authorization token...") 136 | 137 | query_params = parse_qs(env["QUERY_STRING"]) 138 | 139 | if "error" in query_params: 140 | location = "/app?error=" + query_params["error"][0] 141 | return "302 Found", "", {"Location": location} 142 | 143 | self.auth_token = query_params["code"][0] 144 | 145 | print("Received temporary authorization token '%s'" % (self.auth_token,)) 146 | 147 | return "302 Found", "", {"Location": "/app"} 148 | 149 | def _request_auth_token(self): 150 | print("Requesting authorization token...") 151 | 152 | auth_endpoint = self.api_server_url + "/authorize" 153 | query = urlencode({"client_id": "abc", "redirect_uri": self.callback_url, "response_type": "code"}) 154 | 155 | location = "%s?%s" % (auth_endpoint, query) 156 | 157 | return "302 Found", "", {"Location": location} 158 | 159 | def _serve_application(self, env): 160 | query_params = parse_qs(env["QUERY_STRING"]) 161 | 162 | if ("error" in query_params 163 | and query_params["error"][0] == "access_denied"): 164 | return "200 OK", "User has denied access", {} 165 | 166 | if self.access_token_result is None: 167 | if self.auth_token is None: 168 | return self._request_auth_token() 169 | return self._request_access_token() 170 | confirmation = "Current access token '%s' of type '%s'" % (self.access_token, self.token_type) 171 | return "200 OK", str(confirmation), {} 172 | 173 | 174 | def run_app_server(): 175 | app = ClientApplication() 176 | 177 | try: 178 | httpd = make_server('', 8080, app, handler_class=ClientRequestHandler) 179 | 180 | print("Starting Client app on http://localhost:8080/...") 181 | httpd.serve_forever() 182 | except KeyboardInterrupt: 183 | httpd.server_close() 184 | 185 | 186 | def run_auth_server(): 187 | try: 188 | client_store = ClientStore() 189 | client_store.add_client(client_id="abc", client_secret="xyz", 190 | redirect_uris=["http://localhost:8080/callback"]) 191 | 192 | token_store = TokenStore() 193 | 194 | provider = Provider( 195 | access_token_store=token_store, 196 | auth_code_store=token_store, 197 | client_store=client_store, 198 | token_generator=Uuid4TokenGenerator()) 199 | provider.add_grant( 200 | AuthorizationCodeGrant(site_adapter=TestSiteAdapter()) 201 | ) 202 | 203 | app = Application(provider=provider) 204 | 205 | httpd = make_server('', 8081, app, handler_class=OAuthRequestHandler) 206 | 207 | print("Starting OAuth2 server on http://localhost:8081/...") 208 | httpd.serve_forever() 209 | except KeyboardInterrupt: 210 | httpd.server_close() 211 | 212 | 213 | def main(): 214 | auth_server = Process(target=run_auth_server) 215 | auth_server.start() 216 | app_server = Process(target=run_app_server) 217 | app_server.start() 218 | print("Access http://localhost:8080/app in your browser") 219 | 220 | def sigint_handler(signal, frame): 221 | print("Terminating servers...") 222 | auth_server.terminate() 223 | auth_server.join() 224 | app_server.terminate() 225 | app_server.join() 226 | 227 | signal.signal(signal.SIGINT, sigint_handler) 228 | 229 | if __name__ == "__main__": 230 | main() 231 | -------------------------------------------------------------------------------- /docs/examples/base_server.py: -------------------------------------------------------------------------------- 1 | from wsgiref.simple_server import make_server 2 | import oauth2 3 | import oauth2.grant 4 | import oauth2.error 5 | import oauth2.store.memory 6 | import oauth2.tokengenerator 7 | import oauth2.web.wsgi 8 | 9 | 10 | # Create a SiteAdapter to interact with the user. 11 | # This can be used to display confirmation dialogs and the like. 12 | class ExampleSiteAdapter(oauth2.web.AuthorizationCodeGrantSiteAdapter, 13 | oauth2.web.ImplicitGrantSiteAdapter): 14 | def authenticate(self, request, environ, scopes, client): 15 | # Check if the user has granted access 16 | if request.post_param("confirm") == "confirm": 17 | return {} 18 | 19 | raise oauth2.error.UserNotAuthenticated 20 | 21 | def render_auth_page(self, request, response, environ, scopes, client): 22 | response.body = ''' 23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | ''' 31 | return response 32 | 33 | def user_has_denied_access(self, request): 34 | # Check if the user has denied access 35 | if request.post_param("deny") == "deny": 36 | return True 37 | return False 38 | 39 | # Create an in-memory storage to store your client apps. 40 | client_store = oauth2.store.memory.ClientStore() 41 | # Add a client 42 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost/callback"]) 43 | 44 | site_adapter = ExampleSiteAdapter() 45 | 46 | # Create an in-memory storage to store issued tokens. 47 | # LocalTokenStore can store access and auth tokens 48 | token_store = oauth2.store.memory.TokenStore() 49 | 50 | # Create the controller. 51 | provider = oauth2.Provider( 52 | access_token_store=token_store, 53 | auth_code_store=token_store, 54 | client_store=client_store, 55 | token_generator=oauth2.tokengenerator.Uuid4TokenGenerator() 56 | ) 57 | 58 | # Add Grants you want to support 59 | provider.add_grant(oauth2.grant.AuthorizationCodeGrant(site_adapter=site_adapter)) 60 | provider.add_grant(oauth2.grant.ImplicitGrant(site_adapter=site_adapter)) 61 | 62 | # Add refresh token capability and set expiration time of access tokens to 30 days 63 | provider.add_grant(oauth2.grant.RefreshToken(expires_in=2592000)) 64 | 65 | # Wrap the controller with the Wsgi adapter 66 | app = oauth2.web.wsgi.Application(provider=provider) 67 | 68 | if __name__ == "__main__": 69 | httpd = make_server('', 8080, app) 70 | httpd.serve_forever() 71 | -------------------------------------------------------------------------------- /docs/examples/client_credentials_grant.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import sys 4 | from wsgiref.simple_server import WSGIRequestHandler, make_server 5 | 6 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 7 | 8 | from oauth2 import Provider 9 | from oauth2.grant import ClientCredentialsGrant 10 | from oauth2.store.memory import ClientStore, TokenStore 11 | from oauth2.tokengenerator import Uuid4TokenGenerator 12 | from oauth2.web.wsgi import Application 13 | 14 | from multiprocessing import Process 15 | 16 | 17 | class OAuthRequestHandler(WSGIRequestHandler): 18 | """ 19 | Request handler that enables formatting of the log messages on the console. 20 | 21 | This handler is used by the oauth2-stateless application. 22 | """ 23 | def address_string(self): 24 | return "oauth2-stateless" 25 | 26 | 27 | def run_auth_server(): 28 | try: 29 | client_store = ClientStore() 30 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=[]) 31 | 32 | token_store = TokenStore() 33 | token_gen = Uuid4TokenGenerator() 34 | token_gen.expires_in['client_credentials'] = 3600 35 | 36 | auth_controller = Provider( 37 | access_token_store=token_store, 38 | auth_code_store=token_store, 39 | client_store=client_store, 40 | token_generator=token_gen) 41 | auth_controller.add_grant(ClientCredentialsGrant()) 42 | 43 | app = Application(provider=auth_controller) 44 | 45 | httpd = make_server('', 8080, app, handler_class=OAuthRequestHandler) 46 | 47 | print("Starting implicit_grant oauth2 server on http://localhost:8080/...") 48 | httpd.serve_forever() 49 | except KeyboardInterrupt: 50 | httpd.server_close() 51 | 52 | def main(): 53 | auth_server = Process(target=run_auth_server) 54 | auth_server.start() 55 | print("To test getting an auth token, execute the following curl command:") 56 | print("curl --ipv4 -v -X POST -d 'grant_type=client_credentials&client_id=abc&client_secret=xyz' " 57 | "http://localhost:8080/token") 58 | 59 | def sigint_handler(signal, frame): 60 | print("Terminating server...") 61 | auth_server.terminate() 62 | auth_server.join() 63 | 64 | signal.signal(signal.SIGINT, sigint_handler) 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /docs/examples/flask_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import WSGIRequestHandler, make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | from flask import Flask 10 | from oauth2 import Provider 11 | from oauth2.compatibility import json, parse_qs, urlencode 12 | from oauth2.error import UserNotAuthenticated 13 | from oauth2.grant import AuthorizationCodeGrant 14 | from oauth2.store.memory import ClientStore, TokenStore 15 | from oauth2.tokengenerator import Uuid4TokenGenerator 16 | from oauth2.web import AuthorizationCodeGrantSiteAdapter 17 | from oauth2.web.flask import oauth_request_hook 18 | from flask import render_template_string 19 | 20 | from multiprocessing import Process 21 | from urllib.request import urlopen 22 | 23 | 24 | logging.basicConfig(level=logging.DEBUG) 25 | 26 | 27 | class ClientRequestHandler(WSGIRequestHandler): 28 | """ 29 | Request handler that enables formatting of the log messages on the console. 30 | 31 | This handler is used by the client application. 32 | """ 33 | def address_string(self): 34 | return "client app" 35 | 36 | 37 | class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter): 38 | """ 39 | This adapter renders a confirmation page so the user can confirm the auth 40 | request. 41 | """ 42 | 43 | def render_auth_page(self, request, response, environ, scopes, client): 44 | confirmation_template = """ 45 |

confirm

46 |

deny

47 | """ 48 | page_url = request.path + "?" + request.query_string 49 | main_login_body = render_template_string(confirmation_template, url=page_url) 50 | response.body = main_login_body 51 | return response 52 | 53 | def authenticate(self, request, environ, scopes, client): 54 | example_user_id = 123 55 | example_ext_data = {} 56 | if request.method == "GET": 57 | if request.get_param("confirm") == "1": 58 | return None, example_user_id 59 | raise UserNotAuthenticated 60 | 61 | def user_has_denied_access(self, request): 62 | if request.method == "GET": 63 | if request.get_param("confirm") == "0": 64 | return True 65 | return False 66 | 67 | def token(self): 68 | pass 69 | 70 | 71 | class ClientApplication(object): 72 | """ 73 | Very basic application that simulates calls to the API of the 74 | oauth2-stateless app. 75 | """ 76 | callback_url = "http://localhost:8080/callback" 77 | client_id = "abc" 78 | client_secret = "xyz" 79 | api_server_url = "http://localhost:8081" 80 | 81 | def __init__(self): 82 | self.access_token_result = None 83 | self.access_token = None 84 | self.auth_token = None 85 | self.token_type = "" 86 | 87 | def __call__(self, env, start_response): 88 | if env["PATH_INFO"] == "/app": 89 | status, body, headers = self._serve_application(env) 90 | elif env["PATH_INFO"] == "/callback": 91 | status, body, headers = self._read_auth_token(env) 92 | else: 93 | status = "301 Moved" 94 | body = "" 95 | headers = {"Location": "/app"} 96 | 97 | start_response(status, [(header, val) for header, val in headers.items()]) 98 | return [body.encode('utf-8')] 99 | 100 | def _request_access_token(self): 101 | print("Requesting access token...") 102 | 103 | post_params = {"client_id": self.client_id, 104 | "client_secret": self.client_secret, 105 | "code": self.auth_token, 106 | "grant_type": "authorization_code", 107 | "redirect_uri": self.callback_url} 108 | 109 | token_endpoint = self.api_server_url + "/token" 110 | 111 | token_result = urlopen(token_endpoint, urlencode(post_params).encode('utf-8')) 112 | result = json.loads(token_result.read().decode('utf-8')) 113 | 114 | self.access_token_result = result 115 | self.access_token = result["access_token"] 116 | self.token_type = result["token_type"] 117 | 118 | confirmation = "Received access token '%s' of type '%s'" % (self.access_token, self.token_type) 119 | print(confirmation) 120 | return "302 Found", "", {"Location": "/app"} 121 | 122 | def _read_auth_token(self, env): 123 | print("Receiving authorization token...") 124 | 125 | query_params = parse_qs(env["QUERY_STRING"]) 126 | 127 | if "error" in query_params: 128 | location = "/app?error=" + query_params["error"][0] 129 | return "302 Found", "", {"Location": location} 130 | 131 | self.auth_token = query_params["code"][0] 132 | 133 | print("Received temporary authorization token '%s'" % (self.auth_token,)) 134 | return "302 Found", "", {"Location": "/app"} 135 | 136 | def _request_auth_token(self): 137 | print("Requesting authorization token...") 138 | 139 | auth_endpoint = self.api_server_url + "/authorize" 140 | query = urlencode({"client_id": "abc", "redirect_uri": self.callback_url, "response_type": "code"}) 141 | 142 | location = "%s?%s" % (auth_endpoint, query) 143 | return "302 Found", "", {"Location": location} 144 | 145 | def _serve_application(self, env): 146 | query_params = parse_qs(env["QUERY_STRING"]) 147 | if "error" in query_params and query_params["error"][0] == "access_denied": 148 | return "200 OK", "User has denied access", {} 149 | 150 | if self.access_token_result is None: 151 | if self.auth_token is None: 152 | return self._request_auth_token() 153 | return self._request_access_token() 154 | confirmation = "Current access token '%s' of type '%s'" % (self.access_token, self.token_type) 155 | return "200 OK", confirmation, {} 156 | 157 | 158 | def run_app_server(): 159 | app = ClientApplication() 160 | 161 | try: 162 | httpd = make_server('', 8080, app, handler_class=ClientRequestHandler) 163 | 164 | print("Starting Client app on http://localhost:8080/...") 165 | httpd.serve_forever() 166 | except KeyboardInterrupt: 167 | httpd.server_close() 168 | 169 | 170 | def run_auth_server(): 171 | client_store = ClientStore() 172 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost:8080/callback"]) 173 | 174 | token_store = TokenStore() 175 | site_adapter = TestSiteAdapter() 176 | 177 | provider = Provider(access_token_store=token_store, 178 | auth_code_store=token_store, client_store=client_store, 179 | token_generator=Uuid4TokenGenerator()) 180 | provider.add_grant(AuthorizationCodeGrant(site_adapter=site_adapter, unique_token=True, expires_in=20)) 181 | #provider.add_grant(AuthorizationCodeGrant(site_adapter=site_adapter)) 182 | 183 | app = Flask(__name__) 184 | 185 | flask_hook = oauth_request_hook(provider) 186 | app.add_url_rule('/authorize', 'authorize', view_func=flask_hook(site_adapter.authenticate), 187 | methods=['GET', 'POST']) 188 | app.add_url_rule('/token', 'token', view_func=flask_hook(site_adapter.token), methods=['POST']) 189 | 190 | app.run(host='0.0.0.0', port=8081) 191 | print("Starting OAuth2 server on http://localhost:8081/...") 192 | 193 | def main(): 194 | auth_server = Process(target=run_auth_server) 195 | auth_server.start() 196 | app_server = Process(target=run_app_server) 197 | app_server.start() 198 | print("Access http://localhost:8080/app in your browser") 199 | 200 | def sigint_handler(signal, frame): 201 | print("Terminating servers...") 202 | auth_server.terminate() 203 | auth_server.join() 204 | app_server.terminate() 205 | app_server.join() 206 | 207 | signal.signal(signal.SIGINT, sigint_handler) 208 | 209 | if __name__ == "__main__": 210 | main() 211 | -------------------------------------------------------------------------------- /docs/examples/implicit_grant.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | from oauth2 import Provider 10 | from oauth2.error import UserNotAuthenticated 11 | from oauth2.grant import ImplicitGrant 12 | from oauth2.store.memory import ClientStore, TokenStore 13 | from oauth2.tokengenerator import Uuid4TokenGenerator 14 | from oauth2.web import ImplicitGrantSiteAdapter 15 | from oauth2.web.wsgi import Application 16 | 17 | from multiprocessing import Process 18 | 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | 23 | class TestSiteAdapter(ImplicitGrantSiteAdapter): 24 | CONFIRMATION_TEMPLATE = """ 25 | 26 | 27 |

28 | confirm 29 |

30 |

31 | deny 32 |

33 | 34 | 35 | """ 36 | 37 | def render_auth_page(self, request, response, environ, scopes, client): 38 | url = request.path + "?" + request.query_string 39 | # Add check if the user is logged or a redirect to the login page here 40 | response.body = self.CONFIRMATION_TEMPLATE.format(url=url) 41 | return response 42 | 43 | def authenticate(self, request, environ, scopes, client): 44 | example_user_id = 123 45 | example_ext_data = {} 46 | if request.method == "GET": 47 | if request.get_param("confirm") == "1": 48 | return example_ext_data, example_user_id 49 | raise UserNotAuthenticated 50 | 51 | def user_has_denied_access(self, request): 52 | if request.method == "GET": 53 | if request.get_param("confirm") == "0": 54 | return True 55 | return False 56 | 57 | 58 | def run_app_server(): 59 | def application(env, start_response): 60 | """ 61 | Serves the local javascript client 62 | """ 63 | 64 | js_app = """ 65 | 66 | 67 | OAuth2 JS Test App 68 | 69 | 70 | 98 | 99 | 100 | """ 101 | 102 | start_response("200 OK", [("Content-Type", "text/html")]) 103 | return [js_app.encode('utf-8')] 104 | try: 105 | httpd = make_server('', 8080, application) 106 | 107 | print("Starting implicit_grant app server on http://localhost:8080/...") 108 | httpd.serve_forever() 109 | except KeyboardInterrupt: 110 | httpd.server_close() 111 | 112 | 113 | def run_auth_server(): 114 | try: 115 | client_store = ClientStore() 116 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost:8080/"]) 117 | 118 | token_store = TokenStore() 119 | 120 | provider = Provider( 121 | access_token_store=token_store, 122 | auth_code_store=token_store, 123 | client_store=client_store, 124 | token_generator=Uuid4TokenGenerator()) 125 | provider.add_grant(ImplicitGrant(site_adapter=TestSiteAdapter())) 126 | 127 | app = Application(provider=provider) 128 | 129 | httpd = make_server('', 8081, app) 130 | 131 | print("Starting implicit_grant oauth2 server on http://localhost:8081/...") 132 | httpd.serve_forever() 133 | except KeyboardInterrupt: 134 | httpd.server_close() 135 | 136 | 137 | def main(): 138 | auth_server = Process(target=run_auth_server) 139 | auth_server.start() 140 | app_server = Process(target=run_app_server) 141 | app_server.start() 142 | print("Access http://localhost:8080/ to start the auth flow") 143 | 144 | def sigint_handler(signal, frame): 145 | print("Terminating servers...") 146 | auth_server.terminate() 147 | auth_server.join() 148 | app_server.terminate() 149 | app_server.join() 150 | 151 | signal.signal(signal.SIGINT, sigint_handler) 152 | 153 | if __name__ == "__main__": 154 | main() 155 | -------------------------------------------------------------------------------- /docs/examples/pyramid/README.md: -------------------------------------------------------------------------------- 1 | Pyramid integration example for oauth2-stateless 2 | 3 | Integrate the example: 4 | 5 | 1. Put classes in base.py in appropriate packages. 6 | 2. impl.py contains controller and site adapter. Also place both of them in appropriate packages. 7 | 3. Implement "password_auth" method in OAuth2SiteAdapter. 8 | 4. Modify "_get_token_store" and "_get_client_store" methods in UserAuthController 9 | 5. Add "config.add_route('authenticateUser', '/user/token')" to "\__init__\.py" 10 | -------------------------------------------------------------------------------- /docs/examples/pyramid/base.py: -------------------------------------------------------------------------------- 1 | from oauth2.client_authenticator import ClientAuthenticator, request_body 2 | from oauth2.compatibility import json 3 | from oauth2.error import (ClientNotFoundError, OAuthInvalidError, 4 | OAuthInvalidNoRedirectError, ParameterMissingError, 5 | UnsupportedGrantError) 6 | from oauth2.tokengenerator import Uuid4TokenGenerator 7 | from oauth2.web import Response 8 | from pyramid.response import Response as PyramidResponse 9 | 10 | 11 | class Request(): 12 | """ 13 | Contains data of the current HTTP request. 14 | """ 15 | def __init__(self, env): 16 | self.method = env.method 17 | self.params = env.json_body 18 | self.registry = env.registry 19 | self.headers = env.registry 20 | 21 | def post_param(self, name): 22 | return self.params.get(name) 23 | 24 | 25 | class BaseAuthController(object): 26 | 27 | def __init__(self, request, site_adapter): 28 | self.request = Request(request) 29 | self.site_adapter = site_adapter 30 | self.token_generator = Uuid4TokenGenerator() 31 | 32 | self.client_store = self._get_client_store() 33 | self.access_token_store = self._get_token_store() 34 | 35 | self.client_authenticator = ClientAuthenticator(client_store=self.client_store, source=request_body) 36 | self.grant_types = []; 37 | 38 | 39 | @classmethod 40 | def _get_token_store(cls): 41 | NotImplementedError 42 | 43 | @classmethod 44 | def _get_client_store(cls): 45 | NotImplementedError 46 | 47 | def add_grant(self, grant): 48 | """ 49 | Adds a Grant that the provider should support. 50 | 51 | :param grant: An instance of a class that extends 52 | :class:`oauth2.grant.GrantHandlerFactory` 53 | """ 54 | if hasattr(grant, "expires_in"): 55 | self.token_generator.expires_in[grant.grant_type] = grant.expires_in 56 | 57 | if hasattr(grant, "refresh_expires_in"): 58 | self.token_generator.refresh_expires_in = grant.refresh_expires_in 59 | 60 | self.grant_types.append(grant) 61 | 62 | 63 | def _determine_grant_type(self, request): 64 | for grant in self.grant_types: 65 | grant_handler = grant(request, self) 66 | if grant_handler is not None: 67 | return grant_handler 68 | raise UnsupportedGrantError 69 | 70 | 71 | def authenticate(self): 72 | response = Response() 73 | grant_type = self._determine_grant_type(self.request) 74 | grant_type.read_validate_params(self.request) 75 | grant_type.process(self.request, response, {}) 76 | return PyramidResponse(body=response.body, status=response.status_code, content_type="application/json") 77 | -------------------------------------------------------------------------------- /docs/examples/pyramid/impl.py: -------------------------------------------------------------------------------- 1 | from pyramid.view import view_config 2 | 3 | from oauth2.error import UserNotAuthenticated, UserNotExist 4 | from oauth2.web import SiteAdapter 5 | from oauth2.store.redisdb import TokenStore, ClientStore 6 | from oauth2.grant import ResourceOwnerGrant 7 | 8 | from base import BaseAuthController 9 | 10 | import os 11 | import sys 12 | import pyramid 13 | 14 | 15 | class OAuth2SiteAdapter(SiteAdapter): 16 | def authenticate(self, request, environ, scopes): 17 | if request.method == "POST": 18 | if request.post_param("grant_type") == 'password': 19 | return self.password_auth(request) 20 | raise UserNotAuthenticated 21 | 22 | def user_has_denied_access(self, request): 23 | if request.method == "POST": 24 | if request.post_param("confirm") is "0": 25 | return True 26 | return False 27 | 28 | # implement this for resource owner grant 29 | def password_auth(self, request): 30 | session = DBSession() 31 | try: 32 | #validate user credentials 33 | user_id = 123 34 | if True: 35 | return None, user_id 36 | raise UserNotAuthenticated 37 | except: 38 | raise 39 | 40 | 41 | class UserAuthController(BaseAuthController): 42 | def __init__(self, request): 43 | super().__init__(request, OAuth2SiteAdapter()) 44 | self.add_grant(ResourceOwnerGrant(unique_token=True)) 45 | 46 | @classmethod 47 | def _get_token_store(cls): 48 | settings = get_current_registry().settings 49 | return TokenStore(host = 127.0.0.1, port = 6379, db = 1) 50 | 51 | @classmethod 52 | def _get_client_store(cls): 53 | settings = get_current_registry().settings 54 | return ClientStore(host = 127.0.0.1, port = 6379, db = 2) 55 | 56 | # add this route in __init__.py 57 | @view_config(route_name="authenticateUser", renderer="json", request_method="POST") 58 | def authenticate(self): 59 | return super().authenticate() 60 | -------------------------------------------------------------------------------- /docs/examples/resource_owner_grant.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | from oauth2 import Provider 10 | from oauth2.compatibility import json, parse_qs, urlencode 11 | from oauth2.error import UserNotAuthenticated 12 | from oauth2.grant import ResourceOwnerGrant 13 | from oauth2.store.memory import ClientStore, TokenStore 14 | from oauth2.tokengenerator import Uuid4TokenGenerator 15 | from oauth2.web import ResourceOwnerGrantSiteAdapter 16 | from oauth2.web.wsgi import Application 17 | 18 | from multiprocessing import Process 19 | from urllib.request import urlopen 20 | from urllib.error import HTTPError 21 | 22 | 23 | logging.basicConfig(level=logging.DEBUG) 24 | 25 | 26 | class ClientApplication(object): 27 | """ 28 | Very basic application that simulates calls to the API of the 29 | oauth2-stateless app. 30 | """ 31 | client_id = "abc" 32 | client_secret = "xyz" 33 | token_endpoint = "http://localhost:8081/token" 34 | 35 | LOGIN_TEMPLATE = """ 36 | 37 |

Test Login

38 |
39 | {failed_message} 40 |
41 |
42 |
43 | Username (foo): 44 |
45 |
46 | Password (bar): 47 |
48 |
49 | 50 |
51 |
52 | 53 | """ 54 | 55 | SERVER_ERROR_TEMPLATE = """ 56 | 57 |

OAuth2 server responded with an error

58 | Error type: {error_type} 59 | Error description: {error_description} 60 | 61 | """ 62 | 63 | TOKEN_TEMPLATE = """ 64 | 65 |
Access token: {access_token}
66 |
67 | Reset 68 |
69 | 70 | """ 71 | 72 | def __init__(self): 73 | self.token = None 74 | self.token_type = "" 75 | 76 | def __call__(self, env, start_response): 77 | if env["PATH_INFO"] == "/login": 78 | status, body, headers = self._login(failed=env["QUERY_STRING"] == "failed=1") 79 | elif env["PATH_INFO"] == "/": 80 | status, body, headers = self._display_token() 81 | elif env["PATH_INFO"] == "/request_token": 82 | status, body, headers = self._request_token(env) 83 | elif env["PATH_INFO"] == "/reset": 84 | status, body, headers = self._reset() 85 | else: 86 | status = "301 Moved" 87 | body = "" 88 | headers = {"Location": "/"} 89 | 90 | start_response(status, [(header, val) for header, val in headers.items()]) 91 | return [body.encode('utf-8')] 92 | 93 | def _display_token(self): 94 | """ 95 | Display token information or redirect to login prompt if none is 96 | available. 97 | """ 98 | if self.token is None: 99 | return "301 Moved", "", {"Location": "/login"} 100 | 101 | return ("200 OK", self.TOKEN_TEMPLATE.format(access_token=self.token["access_token"]), 102 | {"Content-Type": "text/html"}) 103 | 104 | def _login(self, failed=False): 105 | """ 106 | Login prompt 107 | """ 108 | if failed: 109 | content = self.LOGIN_TEMPLATE.format(failed_message="Login failed") 110 | else: 111 | content = self.LOGIN_TEMPLATE.format(failed_message="") 112 | return "200 OK", content, {"Content-Type": "text/html"} 113 | 114 | def _request_token(self, env): 115 | """ 116 | Retrieves a new access token from the OAuth2 server. 117 | """ 118 | params = {} 119 | 120 | content = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) 121 | post_params = parse_qs(content) 122 | # Convert to dict for easier access 123 | for param, value in post_params.items(): 124 | decoded_param = param.decode('utf-8') 125 | decoded_value = value[0].decode('utf-8') 126 | if decoded_param == "username" or decoded_param == "password": 127 | params[decoded_param] = decoded_value 128 | 129 | params["grant_type"] = "password" 130 | params["client_id"] = self.client_id 131 | params["client_secret"] = self.client_secret 132 | # Request an access token by POSTing a request to the auth server. 133 | try: 134 | token_result = urlopen(self.token_endpoint, urlencode(params).encode('utf-8')) 135 | response = token_result.read().decode('utf-8') 136 | except HTTPError as he: 137 | if he.code == 400: 138 | error_body = json.loads(he.read()) 139 | body = self.SERVER_ERROR_TEMPLATE.format(error_type=error_body["error"], 140 | error_description=error_body["error_description"]) 141 | return "400 Bad Request", body, {"Content-Type": "text/html"} 142 | if he.code == 401: 143 | return "302 Found", "", {"Location": "/login?failed=1"} 144 | 145 | self.token = json.loads(response) 146 | return "301 Moved", "", {"Location": "/"} 147 | 148 | def _reset(self): 149 | self.token = None 150 | return "302 Found", "", {"Location": "/login"} 151 | 152 | 153 | class TestSiteAdapter(ResourceOwnerGrantSiteAdapter): 154 | def authenticate(self, request, environ, scopes, client): 155 | example_user_id = 123 156 | example_ext_data = {} 157 | username = request.post_param("username") 158 | password = request.post_param("password") 159 | # A real world application could connect to a database, try to 160 | # retrieve username and password and compare them against the input 161 | if username == "foo" and password == "bar": 162 | return example_ext_data, example_user_id 163 | 164 | raise UserNotAuthenticated 165 | 166 | 167 | def run_app_server(): 168 | app = ClientApplication() 169 | 170 | try: 171 | httpd = make_server('', 8080, app) 172 | 173 | print("Starting Client app on http://localhost:8080/...") 174 | httpd.serve_forever() 175 | except KeyboardInterrupt: 176 | httpd.server_close() 177 | 178 | 179 | def run_auth_server(): 180 | try: 181 | client_store = ClientStore() 182 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=[]) 183 | 184 | token_store = TokenStore() 185 | 186 | provider = Provider( 187 | access_token_store=token_store, 188 | auth_code_store=token_store, 189 | client_store=client_store, 190 | token_generator=Uuid4TokenGenerator()) 191 | 192 | provider.add_grant(ResourceOwnerGrant(site_adapter=TestSiteAdapter())) 193 | 194 | app = Application(provider=provider) 195 | 196 | httpd = make_server('', 8081, app) 197 | 198 | print("Starting OAuth2 server on http://localhost:8081/...") 199 | httpd.serve_forever() 200 | except KeyboardInterrupt: 201 | httpd.server_close() 202 | 203 | 204 | def main(): 205 | auth_server = Process(target=run_auth_server) 206 | auth_server.start() 207 | app_server = Process(target=run_app_server) 208 | app_server.start() 209 | print("Visit http://localhost:8080/ in your browser") 210 | 211 | def sigint_handler(signal, frame): 212 | print("Terminating servers...") 213 | auth_server.terminate() 214 | auth_server.join() 215 | app_server.terminate() 216 | app_server.join() 217 | 218 | signal.signal(signal.SIGINT, sigint_handler) 219 | 220 | if __name__ == "__main__": 221 | main() 222 | -------------------------------------------------------------------------------- /docs/examples/stateless_client_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | from oauth2 import Provider 10 | from oauth2.error import UserNotAuthenticated 11 | from oauth2.grant import ImplicitGrant 12 | from oauth2.store.memory import ClientStore 13 | from oauth2.store.stateless import TokenStore 14 | from oauth2.tokengenerator import StatelessTokenGenerator 15 | from oauth2.web import ImplicitGrantSiteAdapter 16 | from oauth2.web.wsgi import Application 17 | 18 | from multiprocessing import Process 19 | 20 | 21 | logging.basicConfig(level=logging.DEBUG) 22 | 23 | 24 | class TestSiteAdapter(ImplicitGrantSiteAdapter): 25 | CONFIRMATION_TEMPLATE = """ 26 | 27 | 28 |

29 | confirm 30 |

31 |

32 | deny 33 |

34 | 35 | 36 | """ 37 | 38 | def render_auth_page(self, request, response, environ, scopes, client): 39 | url = request.path + "?" + request.query_string 40 | # Add check if the user is logged or a redirect to the login page here 41 | response.body = self.CONFIRMATION_TEMPLATE.format(url=url) 42 | 43 | return response 44 | 45 | def authenticate(self, request, environ, scopes, client): 46 | example_user_id = 123 47 | example_ext_data = {} 48 | if request.method == "GET": 49 | if request.get_param("confirm") == "1": 50 | return example_ext_data, example_user_id 51 | raise UserNotAuthenticated 52 | 53 | def user_has_denied_access(self, request): 54 | if request.method == "GET": 55 | if request.get_param("confirm") == "0": 56 | return True 57 | return False 58 | 59 | 60 | def run_app_server(): 61 | def application(env, start_response): 62 | """ 63 | Serves the local javascript client 64 | """ 65 | 66 | js_app = """ 67 | 68 | 69 | OAuth2 JS Test App 70 | 71 | 72 | 100 | 101 | 102 | """ 103 | 104 | start_response("200 OK", [("Content-Type", "text/html")]) 105 | 106 | return [js_app.encode('utf-8')] 107 | try: 108 | httpd = make_server('', 8080, application) 109 | 110 | print("Starting implicit_grant app server on http://localhost:8080/...") 111 | httpd.serve_forever() 112 | except KeyboardInterrupt: 113 | httpd.server_close() 114 | 115 | 116 | def run_auth_server(): 117 | try: 118 | client_store = ClientStore() 119 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost:8080/"]) 120 | stateless_token = StatelessTokenGenerator(secret_key='xxx') 121 | token_store = TokenStore(stateless_token) 122 | 123 | provider = Provider( 124 | access_token_store=token_store, 125 | auth_code_store=token_store, 126 | client_store=client_store, 127 | token_generator=stateless_token) 128 | provider.add_grant(ImplicitGrant(site_adapter=TestSiteAdapter())) 129 | 130 | app = Application(provider=provider) 131 | 132 | httpd = make_server('', 8081, app) 133 | 134 | print("Starting implicit_grant oauth2 server on http://localhost:8081/...") 135 | httpd.serve_forever() 136 | except KeyboardInterrupt: 137 | httpd.server_close() 138 | 139 | 140 | def main(): 141 | auth_server = Process(target=run_auth_server) 142 | auth_server.start() 143 | app_server = Process(target=run_app_server) 144 | app_server.start() 145 | print("Access http://localhost:8080/ to start the auth flow") 146 | 147 | def sigint_handler(signal, frame): 148 | print("Terminating servers...") 149 | auth_server.terminate() 150 | auth_server.join() 151 | app_server.terminate() 152 | app_server.join() 153 | 154 | signal.signal(signal.SIGINT, sigint_handler) 155 | 156 | if __name__ == "__main__": 157 | main() 158 | -------------------------------------------------------------------------------- /docs/examples/tornado_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import signal 4 | import sys 5 | from wsgiref.simple_server import WSGIRequestHandler, make_server 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../')) 8 | 9 | from oauth2 import Provider 10 | from oauth2.compatibility import json, parse_qs, urlencode 11 | from oauth2.error import UserNotAuthenticated 12 | from oauth2.grant import AuthorizationCodeGrant 13 | from oauth2.store.memory import ClientStore, TokenStore 14 | from oauth2.tokengenerator import Uuid4TokenGenerator 15 | from oauth2.web import AuthorizationCodeGrantSiteAdapter 16 | from oauth2.web.tornado import OAuth2Handler 17 | from tornado.ioloop import IOLoop 18 | from tornado.web import Application, url 19 | 20 | from multiprocessing import Process 21 | from urllib.request import urlopen 22 | 23 | 24 | logging.basicConfig(level=logging.DEBUG) 25 | 26 | 27 | class ClientRequestHandler(WSGIRequestHandler): 28 | """ 29 | Request handler that enables formatting of the log messages on the console. 30 | 31 | This handler is used by the client application. 32 | """ 33 | def address_string(self): 34 | return "client app" 35 | 36 | 37 | class OAuthRequestHandler(WSGIRequestHandler): 38 | """ 39 | Request handler that enables formatting of the log messages on the console. 40 | 41 | This handler is used by the oauth2-stateless application. 42 | """ 43 | def address_string(self): 44 | return "oauth2-stateless" 45 | 46 | 47 | class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter): 48 | """ 49 | This adapter renders a confirmation page so the user can confirm the auth 50 | request. 51 | """ 52 | 53 | CONFIRMATION_TEMPLATE = """ 54 | 55 | 56 |

57 | confirm 58 |

59 |

60 | deny 61 |

62 | 63 | 64 | """ 65 | 66 | def render_auth_page(self, request, response, environ, scopes, client): 67 | page_url = request.path + "?" + request.query_string 68 | response.body = self.CONFIRMATION_TEMPLATE.format(url=page_url) 69 | 70 | return response 71 | 72 | def authenticate(self, request, environ, scopes, client): 73 | example_user_id = 123 74 | example_ext_data = {} 75 | if request.method == "GET": 76 | if request.get_param("confirm") == "1": 77 | return example_ext_data, example_user_id 78 | raise UserNotAuthenticated 79 | 80 | def user_has_denied_access(self, request): 81 | if request.method == "GET": 82 | if request.get_param("confirm") == "0": 83 | return True 84 | return False 85 | 86 | 87 | class ClientApplication(object): 88 | """ 89 | Very basic application that simulates calls to the API of the 90 | oauth2-stateless app. 91 | """ 92 | callback_url = "http://localhost:8080/callback" 93 | client_id = "abc" 94 | client_secret = "xyz" 95 | api_server_url = "http://localhost:8081" 96 | 97 | def __init__(self): 98 | self.access_token_result = None 99 | self.access_token = None 100 | self.auth_token = None 101 | self.token_type = "" 102 | 103 | def __call__(self, env, start_response): 104 | if env["PATH_INFO"] == "/app": 105 | status, body, headers = self._serve_application(env) 106 | elif env["PATH_INFO"] == "/callback": 107 | status, body, headers = self._read_auth_token(env) 108 | else: 109 | status = "301 Moved" 110 | body = "" 111 | headers = {"Location": "/app"} 112 | 113 | start_response(status, [(header, val) for header, val in headers.items()]) 114 | return [body.encode('utf-8')] 115 | 116 | def _request_access_token(self): 117 | print("Requesting access token...") 118 | 119 | post_params = {"client_id": self.client_id, 120 | "client_secret": self.client_secret, 121 | "code": self.auth_token, 122 | "grant_type": "authorization_code", 123 | "redirect_uri": self.callback_url} 124 | token_endpoint = self.api_server_url + "/token" 125 | 126 | token_result = urlopen(token_endpoint, urlencode(post_params).encode('utf-8')) 127 | result = json.loads(token_result.read().decode('utf-8')) 128 | 129 | self.access_token_result = result 130 | self.access_token = result["access_token"] 131 | self.token_type = result["token_type"] 132 | 133 | confirmation = "Received access token '%s' of type '%s'" % (self.access_token, self.token_type) 134 | print(confirmation) 135 | return "302 Found", "", {"Location": "/app"} 136 | 137 | def _read_auth_token(self, env): 138 | print("Receiving authorization token...") 139 | 140 | query_params = parse_qs(env["QUERY_STRING"]) 141 | 142 | if "error" in query_params: 143 | location = "/app?error=" + query_params["error"][0] 144 | return "302 Found", "", {"Location": location} 145 | 146 | self.auth_token = query_params["code"][0] 147 | 148 | print("Received temporary authorization token '%s'" % (self.auth_token,)) 149 | return "302 Found", "", {"Location": "/app"} 150 | 151 | def _request_auth_token(self): 152 | print("Requesting authorization token...") 153 | 154 | auth_endpoint = self.api_server_url + "/authorize" 155 | query = urlencode({"client_id": "abc", 156 | "redirect_uri": self.callback_url, 157 | "response_type": "code"}) 158 | 159 | location = "%s?%s" % (auth_endpoint, query) 160 | return "302 Found", "", {"Location": location} 161 | 162 | def _serve_application(self, env): 163 | query_params = parse_qs(env["QUERY_STRING"]) 164 | if ("error" in query_params and query_params["error"][0] == "access_denied"): 165 | return "200 OK", "User has denied access", {} 166 | 167 | if self.access_token_result is None: 168 | if self.auth_token is None: 169 | return self._request_auth_token() 170 | return self._request_access_token() 171 | confirmation = "Current access token '%s' of type '%s'" % (self.access_token, self.token_type) 172 | return "200 OK", str(confirmation), {} 173 | 174 | 175 | def run_app_server(): 176 | app = ClientApplication() 177 | 178 | try: 179 | httpd = make_server('', 8080, app, handler_class=ClientRequestHandler) 180 | 181 | print("Starting Client app on http://localhost:8080/...") 182 | httpd.serve_forever() 183 | except KeyboardInterrupt: 184 | httpd.server_close() 185 | 186 | 187 | def run_auth_server(): 188 | client_store = ClientStore() 189 | client_store.add_client(client_id="abc", client_secret="xyz", redirect_uris=["http://localhost:8080/callback"]) 190 | 191 | token_store = TokenStore() 192 | 193 | provider = Provider(access_token_store=token_store, 194 | auth_code_store=token_store, client_store=client_store, 195 | token_generator=Uuid4TokenGenerator()) 196 | provider.add_grant(AuthorizationCodeGrant(site_adapter=TestSiteAdapter())) 197 | 198 | try: 199 | app = Application([ 200 | url(provider.authorize_path, OAuth2Handler, dict(provider=provider)), 201 | url(provider.token_path, OAuth2Handler, dict(provider=provider)), 202 | ]) 203 | 204 | app.listen(8081) 205 | print("Starting OAuth2 server on http://localhost:8081/...") 206 | IOLoop.current().start() 207 | except KeyboardInterrupt: 208 | IOLoop.close() 209 | 210 | def main(): 211 | auth_server = Process(target=run_auth_server) 212 | auth_server.start() 213 | app_server = Process(target=run_app_server) 214 | app_server.start() 215 | print("Access http://localhost:8080/app in your browser") 216 | 217 | def sigint_handler(signal, frame): 218 | print("Terminating servers...") 219 | auth_server.terminate() 220 | auth_server.join() 221 | app_server.terminate() 222 | app_server.join() 223 | 224 | signal.signal(signal.SIGINT, sigint_handler) 225 | 226 | if __name__ == "__main__": 227 | main() 228 | -------------------------------------------------------------------------------- /docs/flask.rst: -------------------------------------------------------------------------------- 1 | Flask 2 | ======= 3 | 4 | .. automodule:: oauth2.web.flask 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/frameworks.rst: -------------------------------------------------------------------------------- 1 | Using ``oauth2-stateless`` with other frameworks 2 | ============================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | aiohttp.rst 8 | flask.rst 9 | tornado.rst 10 | -------------------------------------------------------------------------------- /docs/grant.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.grant`` --- Grant classes and helpers 2 | ============================================== 3 | 4 | .. automodule:: oauth2.grant 5 | 6 | Helpers and base classes 7 | ------------------------ 8 | 9 | .. autoclass:: GrantHandlerFactory 10 | 11 | .. autoclass:: ScopeGrant 12 | 13 | .. autoclass:: Scope 14 | :members: parse 15 | 16 | .. autoclass:: SiteAdapterMixin 17 | 18 | Grant classes 19 | ------------- 20 | 21 | .. autoclass:: AuthorizationCodeGrant 22 | :show-inheritance: 23 | 24 | .. autoclass:: ImplicitGrant 25 | :show-inheritance: 26 | 27 | .. autoclass:: ResourceOwnerGrant 28 | :show-inheritance: 29 | 30 | .. autoclass:: RefreshToken 31 | :show-inheritance: 32 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. title:: oauth2-stateless 2 | 3 | .. automodule:: oauth2 4 | 5 | Contents: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | grant.rst 11 | store.rst 12 | oauth2.rst 13 | web.rst 14 | client_authenticator.rst 15 | token_generator.rst 16 | log.rst 17 | error.rst 18 | unique_token.rst 19 | frameworks.rst 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/log.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.log`` --- Logging 2 | ========================== 3 | 4 | .. automodule:: oauth2.log 5 | -------------------------------------------------------------------------------- /docs/oauth2.rst: -------------------------------------------------------------------------------- 1 | ``oauth2`` --- Provider Class 2 | ============================= 3 | 4 | .. autoclass:: oauth2.Provider 5 | :members: scope_separator, add_grant, dispatch, enable_unique_tokens 6 | -------------------------------------------------------------------------------- /docs/store.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store`` --- Storing and retrieving data 2 | ================================================ 3 | 4 | .. automodule:: oauth2.store 5 | 6 | Data Types 7 | ---------- 8 | 9 | .. autoclass:: oauth2.datatype.AccessToken 10 | 11 | .. autoclass:: oauth2.datatype.AuthorizationCode 12 | 13 | .. autoclass:: oauth2.datatype.Client 14 | 15 | Base Classes 16 | ------------ 17 | 18 | .. autoclass:: AccessTokenStore 19 | :members: 20 | 21 | .. autoclass:: AuthCodeStore 22 | :members: 23 | 24 | .. autoclass:: ClientStore 25 | :members: 26 | 27 | Implementations 28 | --------------- 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | store/memcache.rst 34 | store/memory.rst 35 | store/mongodb.rst 36 | store/redisdb.rst 37 | store/dynamodb.rst 38 | store/dbapi.rst 39 | store/mysql.rst 40 | -------------------------------------------------------------------------------- /docs/store/dbapi.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.dbapi`` --- PEP249 compatible stores 2 | =================================================== 3 | 4 | .. automodule:: oauth2.store.dbapi 5 | 6 | .. autoclass:: oauth2.store.dbapi.DatabaseStore 7 | 8 | .. autoclass:: oauth2.store.dbapi.DbApiAccessTokenStore 9 | :members: 10 | 11 | .. autoclass:: oauth2.store.dbapi.DbApiAuthCodeStore 12 | :members: 13 | 14 | .. autoclass:: oauth2.store.dbapi.DbApiClientStore 15 | :members: 16 | -------------------------------------------------------------------------------- /docs/store/dynamodb.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.dynamodb`` --- Dynamodb store adapters 2 | ===================================================== 3 | 4 | .. automodule:: oauth2.store.dynamodb 5 | 6 | .. autoclass:: TokenStore 7 | -------------------------------------------------------------------------------- /docs/store/memcache.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.memcache`` --- Memcache store adapters 2 | ===================================================== 3 | 4 | .. automodule:: oauth2.store.memcache 5 | 6 | .. autoclass:: TokenStore 7 | -------------------------------------------------------------------------------- /docs/store/memory.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.memory`` --- In-memory store adapters 2 | ===================================================== 3 | 4 | .. automodule:: oauth2.store.memory 5 | 6 | .. autoclass:: ClientStore 7 | :members: 8 | 9 | .. autoclass:: TokenStore 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/store/mongodb.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.mongodb`` --- Mongodb store adapters 2 | =================================================== 3 | 4 | .. automodule:: oauth2.store.mongodb 5 | 6 | .. autoclass:: oauth2.store.mongodb.MongodbStore 7 | 8 | .. autoclass:: oauth2.store.mongodb.AccessTokenStore 9 | 10 | .. autoclass:: oauth2.store.mongodb.AuthCodeStore 11 | 12 | .. autoclass:: oauth2.store.mongodb.ClientStore 13 | -------------------------------------------------------------------------------- /docs/store/mysql.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.dbapi.mysql`` --- Mysql store adapters 2 | ===================================================== 3 | 4 | .. automodule:: oauth2.store.dbapi.mysql 5 | 6 | .. autoclass:: MysqlAccessTokenStore 7 | 8 | .. autoclass:: MysqlAuthCodeStore 9 | 10 | .. autoclass:: MysqlClientStore 11 | -------------------------------------------------------------------------------- /docs/store/redisdb.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.store.redisdb`` --- Redis store adapters 2 | ================================================= 3 | 4 | .. automodule:: oauth2.store.redisdb 5 | 6 | .. autoclass:: oauth2.store.redisdb.TokenStore 7 | 8 | .. autoclass:: oauth2.store.redisdb.ClientStore 9 | -------------------------------------------------------------------------------- /docs/token_generator.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.tokengenerator`` --- Generate Tokens 2 | ============================================= 3 | 4 | .. automodule:: oauth2.tokengenerator 5 | 6 | Base Class 7 | ---------- 8 | 9 | .. autoclass:: TokenGenerator 10 | :members: 11 | 12 | Implementations 13 | --------------- 14 | 15 | .. autoclass:: StatelessTokenGenerator 16 | :members: 17 | :show-inheritance: 18 | 19 | .. autoclass:: URandomTokenGenerator 20 | :members: 21 | :show-inheritance: 22 | 23 | .. autoclass:: Uuid4TokenGenerator 24 | :members: 25 | :show-inheritance: 26 | -------------------------------------------------------------------------------- /docs/tornado.rst: -------------------------------------------------------------------------------- 1 | Tornado 2 | ======= 3 | 4 | .. automodule:: oauth2.web.tornado 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/unique_token.rst: -------------------------------------------------------------------------------- 1 | Unique Access Tokens 2 | ==================== 3 | 4 | This page explains the concepts of unique access tokens and how to enable this 5 | feature. 6 | 7 | What are unique access tokens? 8 | ------------------------------ 9 | 10 | When the use of unique access tokens is enabled the Provider will respond with 11 | an existing access token to subsequent requests of a client instead of issuing 12 | a new token on each request. 13 | 14 | An existing access token will be returned if the following conditions are 15 | met: 16 | 17 | * The access token has been issued for the requesting client 18 | * The access token has been issued for the same user as in the current request 19 | * The requested scope is the same as in the existing access token 20 | * The requested type is the same as in the existing access token 21 | 22 | .. note:: 23 | 24 | Unique access tokens are currently supported by 25 | :class:`oauth2.grant.AuthorizationCodeGrant` and 26 | :class:`oauth2.grant.ResourceOwnerGrant`. 27 | 28 | Preconditions 29 | ------------- 30 | 31 | As stated in the previous section, a unique access token is bound not only to a 32 | client but also to a user. To make this work the Provider needs some kind of 33 | identifier that is unique for each user (typically the ID of a user in the 34 | database). The identifier is stored along with all the other information of an 35 | access token. It has to be returned as the second item of a tuple by your 36 | implementation of :class:`oauth2.web.AuthenticatingSiteAdapter.authenticate`:: 37 | 38 | class MySiteAdapter(SiteAdapter): 39 | 40 | def authenticate(self, request, environ, scopes): 41 | // Your logic here 42 | 43 | return None, user["id"] 44 | 45 | Enabling the feature 46 | -------------------- 47 | 48 | Unique access tokens are turned off by default. They can be turned on for each 49 | grant individually:: 50 | 51 | auth_code_grant = oauth2.grant.AuthorizationCodeGrant(unique_token=True) 52 | provider = oauth2.Provider() // Parameters omitted for readability 53 | provider.add_grant(auth_code_grant) 54 | 55 | or you can enable them for all grants that support this feature after 56 | initialization of :class:`oauth2.Provider`:: 57 | 58 | provider = oauth2.Provider() // Parameters omitted for readability 59 | provider.enable_unique_tokens() 60 | 61 | .. note:: 62 | 63 | If you enable the feature but forgot to make 64 | :class:`oauth2.web.AuthenticatingSiteAdapter.authenticate` return a user 65 | identifier, the Provider will respond with an error to requests for a 66 | token. 67 | -------------------------------------------------------------------------------- /docs/web.rst: -------------------------------------------------------------------------------- 1 | ``oauth2.web`` --- Interaction over HTTP 2 | ======================================== 3 | 4 | .. automodule:: oauth2.web 5 | 6 | Site adapters 7 | ------------- 8 | 9 | .. autoclass:: UserFacingSiteAdapter 10 | :members: 11 | 12 | .. autoclass:: AuthenticatingSiteAdapter 13 | :members: 14 | 15 | .. autoclass:: AuthorizationCodeGrantSiteAdapter 16 | :members: 17 | :inherited-members: 18 | :show-inheritance: 19 | 20 | .. autoclass:: ImplicitGrantSiteAdapter 21 | :members: 22 | :inherited-members: 23 | :show-inheritance: 24 | 25 | .. autoclass:: ResourceOwnerGrantSiteAdapter 26 | :members: 27 | :inherited-members: 28 | :show-inheritance: 29 | 30 | HTTP flow 31 | --------- 32 | 33 | .. autoclass:: Request 34 | :members: 35 | 36 | .. autoclass:: Response 37 | :members: 38 | -------------------------------------------------------------------------------- /oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============= 3 | oauth2-stateless 4 | ============= 5 | 6 | oauth2-stateless is a framework that aims at making it easy to provide 7 | authentication via `OAuth 2.0 `_ within 8 | an application stack. 9 | 10 | Usage 11 | ===== 12 | 13 | Example: 14 | 15 | .. literalinclude:: examples/base_server.py 16 | 17 | Installation 18 | ============ 19 | 20 | oauth2-stateless is available on 21 | `PyPI `_:: 22 | 23 | pip install oauth2-stateless 24 | """ 25 | 26 | from oauth2.client_authenticator import ClientAuthenticator, request_body 27 | from oauth2.compatibility import json 28 | from oauth2.error import (ClientNotFoundError, OAuthInvalidError, 29 | OAuthInvalidNoRedirectError, UnsupportedGrantError) 30 | from oauth2.grant import (AuthorizationCodeGrant, ClientCredentialsGrant, 31 | ImplicitGrant, RefreshToken, ResourceOwnerGrant, 32 | Scope) 33 | from oauth2.log import app_log 34 | from oauth2.tokengenerator import Uuid4TokenGenerator 35 | from oauth2.web import Response 36 | 37 | 38 | class Provider(object): 39 | """ 40 | Endpoint of requests to the OAuth 2.0 provider. 41 | 42 | :param access_token_store: An object that implements methods defined by :class:`oauth2.store.AccessTokenStore`. 43 | :type access_token_store: oauth2.store.AccessTokenStore 44 | 45 | :param auth_code_store: An object that implements methods defined by :class:`oauth2.store.AuthCodeStore`. 46 | :type auth_code_store: oauth2.store.AuthCodeStore 47 | 48 | :param client_store: An object that implements methods defined by :class:`oauth2.store.ClientStore`. 49 | :type client_store: oauth2.store.ClientStore 50 | 51 | :param token_generator: Object to generate unique tokens. 52 | :type token_generator: oauth2.tokengenerator.TokenGenerator 53 | 54 | :param client_authentication_source: A callable which when executed, authenticates a client. 55 | See :mod:`oauth2.client_authenticator`. 56 | :type client_authentication_source: callable 57 | 58 | :param response_class: Class of the response object. Defaults to :class:`oauth2.web.Response`. 59 | :type response_class: oauth2.web.Response 60 | """ 61 | authorize_path = "/authorize" 62 | token_path = "/token" 63 | 64 | def __init__(self, access_token_store, auth_code_store, client_store, 65 | token_generator, client_authentication_source=request_body, 66 | response_class=Response): 67 | self.grant_types = [] 68 | self._input_handler = None 69 | 70 | self.access_token_store = access_token_store 71 | self.auth_code_store = auth_code_store 72 | self.client_authenticator = ClientAuthenticator(client_store=client_store, 73 | source=client_authentication_source) 74 | self.response_class = response_class 75 | self.token_generator = token_generator 76 | 77 | def add_grant(self, grant): 78 | """ 79 | Adds a Grant that the provider should support. 80 | 81 | :param grant: An instance of a class that extends :class:`oauth2.grant.GrantHandlerFactory` 82 | :type grant: oauth2.grant.GrantHandlerFactory 83 | """ 84 | if hasattr(grant, "expires_in"): 85 | self.token_generator.expires_in[grant.grant_type] = grant.expires_in 86 | 87 | if hasattr(grant, "refresh_expires_in"): 88 | self.token_generator.refresh_expires_in = grant.refresh_expires_in 89 | 90 | self.grant_types.append(grant) 91 | 92 | def dispatch(self, request, environ): 93 | """ 94 | Checks which Grant supports the current request and dispatches to it. 95 | 96 | :param request: The incoming request. 97 | :type request: :class:`oauth2.web.Request` 98 | 99 | :param environ: Dict containing variables of the environment. 100 | :type environ: dict 101 | 102 | :return: An instance of ``oauth2.web.Response``. 103 | """ 104 | try: 105 | grant_type = self._determine_grant_type(request) 106 | 107 | response = self.response_class() 108 | 109 | grant_type.read_validate_params(request) 110 | 111 | return grant_type.process(request, response, environ) 112 | except OAuthInvalidNoRedirectError: 113 | response = self.response_class() 114 | response.add_header("Content-Type", "application/json") 115 | response.status_code = 400 116 | response.body = json.dumps({ 117 | "error": "invalid_redirect_uri", 118 | "error_description": "Invalid redirect URI" 119 | }) 120 | return response 121 | except OAuthInvalidError as err: 122 | response = self.response_class() 123 | return grant_type.handle_error(error=err, response=response) 124 | except UnsupportedGrantError: 125 | response = self.response_class() 126 | response.add_header("Content-Type", "application/json") 127 | response.status_code = 400 128 | response.body = json.dumps({ 129 | "error": "unsupported_response_type", 130 | "error_description": "Grant not supported" 131 | }) 132 | return response 133 | except: 134 | app_log.error("Uncaught Exception", exc_info=True) 135 | response = self.response_class() 136 | return grant_type.handle_error( 137 | error=OAuthInvalidError(error="server_error", explanation="Internal server error"), 138 | response=response) 139 | 140 | def enable_unique_tokens(self): 141 | """ 142 | Enable the use of unique access tokens on all grant types that support this option. 143 | """ 144 | for grant_type in self.grant_types: 145 | if hasattr(grant_type, "unique_token"): 146 | grant_type.unique_token = True 147 | 148 | @property 149 | def scope_separator(self, separator): 150 | """ 151 | Sets the separator of values in the scope query parameter. 152 | Defaults to " " (whitespace). 153 | 154 | The following code makes the Provider use "," instead of " ":: 155 | 156 | provider = Provider() 157 | provider.scope_separator = "," 158 | 159 | Now the scope parameter in the request of a client can look like this: 160 | `scope=foo,bar`. 161 | """ 162 | Scope.separator = separator 163 | 164 | def _determine_grant_type(self, request): 165 | for grant in self.grant_types: 166 | grant_handler = grant(request, self) 167 | if grant_handler is not None: 168 | return grant_handler 169 | 170 | raise UnsupportedGrantError 171 | -------------------------------------------------------------------------------- /oauth2/client_authenticator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Every client that sends a request to obtain an access token needs to 3 | authenticate with the provider. 4 | 5 | The authentication of confidential clients can be handled in several ways, 6 | some of which come bundled with this module. 7 | """ 8 | 9 | from base64 import b64decode 10 | 11 | from oauth2.error import (ClientNotFoundError, OAuthInvalidError, 12 | OAuthInvalidNoRedirectError, RedirectUriUnknown) 13 | 14 | 15 | class ClientAuthenticator(object): 16 | """ 17 | Handles authentication of a client both by its identifier as well as by its identifier and secret. 18 | 19 | :param client_store: The Client Store to retrieve a client from. 20 | :type client_store: oauth2.store.ClientStore 21 | 22 | :param source: A callable that returns a tuple (, ) 23 | :type source: callable 24 | """ 25 | def __init__(self, client_store, source): 26 | self.client_store = client_store 27 | self.source = source 28 | 29 | def by_identifier(self, request): 30 | """ 31 | Authenticates a client by its identifier. 32 | 33 | :param request: The incoming request 34 | :type request: oauth2.web.Request 35 | 36 | :return: The identified client 37 | :rtype: oauth2.datatype.Client 38 | 39 | :raises: :class OAuthInvalidNoRedirectError: 40 | """ 41 | client_id = request.get_param("client_id") 42 | 43 | if client_id is None: 44 | raise OAuthInvalidNoRedirectError(error="missing_client_id") 45 | 46 | try: 47 | client = self.client_store.fetch_by_client_id(client_id) 48 | except ClientNotFoundError: 49 | raise OAuthInvalidNoRedirectError(error="unknown_client") 50 | 51 | redirect_uri = request.get_param("redirect_uri") 52 | if redirect_uri is not None: 53 | try: 54 | client.redirect_uri = redirect_uri 55 | except RedirectUriUnknown: 56 | raise OAuthInvalidNoRedirectError( 57 | error="invalid_redirect_uri") 58 | 59 | return client 60 | 61 | def by_identifier_secret(self, request): 62 | """ 63 | Authenticates a client by its identifier and secret (aka password). 64 | 65 | :param request: The incoming request 66 | :type request: oauth2.web.Request 67 | 68 | :return: The identified client 69 | :rtype: oauth2.datatype.Client 70 | 71 | :raises OAuthInvalidError: If the client could not be found, is not allowed to to use the current grant or 72 | supplied invalid credentials 73 | """ 74 | client_id, client_secret = self.source(request=request) 75 | 76 | try: 77 | client = self.client_store.fetch_by_client_id(client_id) 78 | except ClientNotFoundError: 79 | raise OAuthInvalidError(error="invalid_client", explanation="No client could be found") 80 | 81 | grant_type = request.post_param("grant_type") 82 | if client.grant_type_supported(grant_type) is False: 83 | raise OAuthInvalidError(error="unauthorized_client", 84 | explanation="The client is not allowed to use this grant type") 85 | 86 | if client.secret != client_secret: 87 | raise OAuthInvalidError(error="invalid_client", explanation="Invalid client credentials") 88 | 89 | return client 90 | 91 | 92 | def request_body(request): 93 | """ 94 | Extracts the credentials of a client from the 95 | *application/x-www-form-urlencoded* body of a request. 96 | 97 | Expects the client_id to be the value of the ``client_id`` parameter and 98 | the client_secret to be the value of the ``client_secret`` parameter. 99 | 100 | :param request: The incoming request 101 | :type request: oauth2.web.Request 102 | 103 | :return: A tuple in the format of `(, )` 104 | :rtype: tuple 105 | """ 106 | client_id = request.post_param("client_id") 107 | if client_id is None: 108 | raise OAuthInvalidError(error="invalid_request", explanation="Missing client identifier") 109 | 110 | client_secret = request.post_param("client_secret") 111 | if client_secret is None: 112 | raise OAuthInvalidError(error="invalid_request", explanation="Missing client credentials") 113 | 114 | return client_id, client_secret 115 | 116 | 117 | def http_basic_auth(request): 118 | """ 119 | Extracts the credentials of a client using HTTP Basic Auth. 120 | 121 | Expects the ``client_id`` to be the username and the ``client_secret`` to 122 | be the password part of the Authorization header. 123 | 124 | :param request: The incoming request 125 | :type request: oauth2.web.Request 126 | 127 | :return: A tuple in the format of (, )` 128 | :rtype: tuple 129 | """ 130 | auth_header = request.header("authorization") 131 | 132 | if auth_header is None: 133 | raise OAuthInvalidError(error="invalid_request", explanation="Authorization header is missing") 134 | 135 | auth_parts = auth_header.strip().encode("latin1").split(None) 136 | 137 | if auth_parts[0].strip().lower() != b'basic': 138 | raise OAuthInvalidError(error="invalid_request", explanation="Provider supports basic authentication only") 139 | 140 | client_id, client_secret = b64decode(auth_parts[1]).split(b':', 1) 141 | 142 | return client_id.decode("latin1"), client_secret.decode("latin1") 143 | -------------------------------------------------------------------------------- /oauth2/compatibility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensures compatibility of libraries 3 | and The availability of which depends on the developer's preferences. 4 | """ 5 | 6 | from urllib.parse import parse_qs # pragma: no cover 7 | from urllib.parse import urlencode # pragma: no cover 8 | from urllib.parse import quote # pragma: no cover 9 | 10 | try: 11 | import ujson as json # pragma: no cover 12 | except ImportError: 13 | import json # pragma: no cover 14 | -------------------------------------------------------------------------------- /oauth2/datatype.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Definitions of types used by grants. 4 | """ 5 | 6 | import time 7 | 8 | from oauth2.error import RedirectUriUnknown 9 | 10 | 11 | class AccessToken(object): 12 | """ 13 | An access token and associated data. 14 | """ 15 | def __init__(self, client_id, grant_type, token, data={}, expires_at=None, 16 | refresh_token=None, refresh_expires_at=None, scopes=[], user_id=None): 17 | self.client_id = client_id 18 | self.grant_type = grant_type 19 | self.token = token 20 | self.data = data 21 | self.expires_at = expires_at 22 | self.refresh_token = refresh_token 23 | self.refresh_expires_at = refresh_expires_at 24 | self.scopes = scopes 25 | self.user_id = user_id 26 | 27 | @property 28 | def expires_in(self): 29 | """ 30 | Returns the time until the token expires. 31 | 32 | :return: The remaining time until expiration in seconds or 0 if the token has expired. 33 | """ 34 | time_left = self.expires_at - int(time.time()) 35 | 36 | if time_left > 0: 37 | return time_left 38 | return 0 39 | 40 | def is_expired(self): 41 | """ 42 | Determines if the token has expired. 43 | 44 | :return: `True` if the token has expired. Otherwise `False`. 45 | """ 46 | if self.expires_at is None or self.expires_in > 0: 47 | return False 48 | 49 | return True 50 | 51 | 52 | class AuthorizationCode(object): 53 | """ 54 | Holds an authorization code and additional information. 55 | """ 56 | def __init__(self, client_id, code, expires_at, redirect_uri, scopes, 57 | data=None, user_id=None): 58 | self.client_id = client_id 59 | self.code = code 60 | self.data = data 61 | self.expires_at = expires_at 62 | self.redirect_uri = redirect_uri 63 | self.scopes = scopes 64 | self.user_id = user_id 65 | 66 | def is_expired(self): 67 | if self.expires_at < int(time.time()): 68 | return True 69 | return False 70 | 71 | 72 | class Client(object): 73 | """ 74 | Representation of a client application. 75 | """ 76 | def __init__(self, identifier, secret, authorized_grants=None, 77 | authorized_response_types=None, redirect_uris=None): 78 | """ 79 | :param identifier: The unique identifier of a client. 80 | :param secret: The secret the clients uses to authenticate. 81 | :param authorized_grants: A list of grants under which the client can request tokens. 82 | All grants are allowed if this value is set to `None` (default). 83 | :param authorized_response_types: A list of response types of which the client can request tokens. 84 | All response types are allowed if this value is set to `None` (default). 85 | :redirect_uris: A list of redirect uris this client can use. 86 | """ 87 | self.authorized_grants = authorized_grants 88 | self.authorized_response_types = authorized_response_types 89 | self.identifier = identifier 90 | self.secret = secret 91 | 92 | self.redirect_uris = redirect_uris if redirect_uris else [] 93 | self._redirect_uri = None 94 | 95 | @property 96 | def redirect_uri(self): 97 | if self._redirect_uri is None: 98 | # redirect_uri is an optional param. 99 | # If not supplied, we use the first entry stored in db as default. 100 | return self.redirect_uris[0] 101 | return self._redirect_uri 102 | 103 | @redirect_uri.setter 104 | def redirect_uri(self, value): 105 | if value not in self.redirect_uris: 106 | raise RedirectUriUnknown 107 | self._redirect_uri = value 108 | 109 | def grant_type_supported(self, grant_type): 110 | """ 111 | Checks if the Client is authorized receive tokens for the given grant. 112 | 113 | :param grant_type: The type of the grant. 114 | 115 | :return: Boolean 116 | """ 117 | if self.authorized_grants is None: 118 | return True 119 | 120 | return grant_type in self.authorized_grants 121 | 122 | def response_type_supported(self, response_type): 123 | """ 124 | Checks if the client is allowed to receive tokens for the given response type. 125 | 126 | :param response_type: The response type. 127 | 128 | :return: Boolean 129 | """ 130 | if self.authorized_response_types is None: 131 | return True 132 | 133 | return response_type in self.authorized_response_types 134 | -------------------------------------------------------------------------------- /oauth2/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Errors raised during the OAuth 2.0 flow. 3 | """ 4 | 5 | 6 | class AccessTokenNotFound(Exception): 7 | """ 8 | Error indicating that an access token could not be read from the 9 | storage backend by an instance of :class:`oauth2.store.AccessTokenStore`. 10 | """ 11 | pass 12 | 13 | 14 | class AuthCodeNotFound(Exception): 15 | """ 16 | Error indicating that an authorization code could not be read from the 17 | storage backend by an instance of :class:`oauth2.store.AuthCodeStore`. 18 | """ 19 | pass 20 | 21 | 22 | class ClientNotFoundError(Exception): 23 | """ 24 | Error raised by an implementation of :class:`oauth2.store.ClientStore` if a client does not exists. 25 | """ 26 | pass 27 | 28 | 29 | class InvalidSiteAdapter(Exception): 30 | """ 31 | Raised by :class:`oauth2.grant.SiteAdapterMixin` in case an invalid site adapter was passed to the instance. 32 | """ 33 | pass 34 | 35 | 36 | class UserIdentifierMissingError(Exception): 37 | """ 38 | Indicates that the identifier of a user is missing when the use of unique access token is enabled. 39 | """ 40 | pass 41 | 42 | 43 | class OAuthBaseError(Exception): 44 | """ 45 | Base class used by all OAuth 2.0 errors. 46 | 47 | :param error: Identifier of the error. 48 | :param error_uri: Set this to delivery an URL to your documentation that describes the error. (optional) 49 | :param explanation: Short message that describes the error. (optional) 50 | """ 51 | def __init__(self, error, error_uri=None, explanation=None): 52 | self.error = error 53 | self.error_uri = error_uri 54 | self.explanation = explanation 55 | 56 | super().__init__() 57 | 58 | 59 | class OAuthInvalidError(OAuthBaseError): 60 | """ 61 | Indicates an error during validation of a request. 62 | """ 63 | pass 64 | 65 | 66 | class OAuthInvalidNoRedirectError(OAuthInvalidError): 67 | """ 68 | Indicates an error during validation of a request. 69 | The provider will not inform the client about the error by redirecting to it. 70 | This behaviour is required by the Authorization Request step of the Authorization Code Grant and Implicit Grant. 71 | """ 72 | pass 73 | 74 | 75 | class UnsupportedGrantError(Exception): 76 | """ 77 | Indicates that a requested grant is not supported by the server. 78 | """ 79 | pass 80 | 81 | 82 | class RedirectUriUnknown(Exception): 83 | """ 84 | Indicates that a redirect_uri is not associated with a client. 85 | """ 86 | pass 87 | 88 | 89 | class UserNotAuthenticated(Exception): 90 | """ 91 | Raised by a :class:`oauth2.web.SiteAdapter` if a user could not be authenticated. 92 | """ 93 | pass 94 | -------------------------------------------------------------------------------- /oauth2/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging support 3 | 4 | There are two loggers available: 5 | 6 | * ``oauth2.application``: Logging of uncaught exceptions 7 | * ``oauth2.general``: General purpose logging of debug errors and warnings 8 | 9 | If logging has not been configured, you will likely see this error: 10 | 11 | .. code-block:: python 12 | 13 | No handlers could be found for logger "oauth2.application" 14 | 15 | Make sure that logging is configured to avoid this: 16 | 17 | .. code-block:: python 18 | 19 | import logging 20 | logging.basicConfig() 21 | """ 22 | import logging 23 | 24 | app_log = logging.getLogger("oauth2.application") 25 | gen_log = logging.getLogger("oauth2.general") 26 | -------------------------------------------------------------------------------- /oauth2/store/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Store adapters to persist and retrieve data during the OAuth 2.0 process or for later use. 3 | This module provides base classes that can be extended to implement your own solution specific to your needs. 4 | It also includes implementations for popular storage systems like memcache. 5 | """ 6 | 7 | 8 | class AccessTokenStore(object): 9 | """ 10 | Base class for persisting an access token after it has been generated. 11 | Used by two-legged and three-legged authentication flows. 12 | """ 13 | 14 | def save_token(self, access_token): 15 | """ 16 | Stores an access token and additional data. 17 | 18 | :param access_token: An instance of :class:`oauth2.datatype.AccessToken`. 19 | """ 20 | raise NotImplementedError 21 | 22 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 23 | """ 24 | Fetches an access token identified by its client id, type of grant and user id. 25 | This method must be implemented to make use of unique access tokens. 26 | 27 | :param client_id: Identifier of the client a token belongs to. 28 | :param grant_type: The type of the grant that created the token 29 | :param user_id: Identifier of the user a token belongs to. 30 | :return: An instance of :class:`oauth2.datatype.AccessToken`. 31 | :raises: :class:`oauth2.error.AccessTokenNotFound` if no data could be retrieved. 32 | """ 33 | raise NotImplementedError 34 | 35 | def fetch_by_refresh_token(self, refresh_token): 36 | """ 37 | Fetches an access token from the store using its refresh token to identify it. 38 | 39 | :param refresh_token: A string containing the refresh token. 40 | :return: An instance of :class:`oauth2.datatype.AccessToken`. 41 | :raises: :class:`oauth2.error.AccessTokenNotFound` if no data could be retrieved for given refresh_token. 42 | """ 43 | raise NotImplementedError 44 | 45 | def delete_refresh_token(self, refresh_token): 46 | """ 47 | Deletes an access token from the store using its refresh token to identify it. 48 | This invalidates both the access token and the refresh token. 49 | 50 | :param refresh_token: A string containing the refresh token. 51 | :return: None. 52 | :raises: :class:`oauth2.error.AccessTokenNotFound` if no data could be retrieved for given refresh_token. 53 | """ 54 | raise NotImplementedError 55 | 56 | 57 | class AuthCodeStore(object): 58 | """ 59 | Base class for persisting and retrieving an auth token during the 60 | Authorization Code Grant flow. 61 | """ 62 | def fetch_by_code(self, code): 63 | """ 64 | Returns an AuthorizationCode fetched from a storage. 65 | 66 | :param code: The authorization code. 67 | :return: An instance of :class:`oauth2.datatype.AuthorizationCode`. 68 | :raises: :class:`oauth2.error.AuthCodeNotFound` if no data could be retrieved for given code. 69 | """ 70 | raise NotImplementedError 71 | 72 | def save_code(self, authorization_code): 73 | """ 74 | Stores the data belonging to an authorization code token. 75 | 76 | :param authorization_code: An instance of :class:`oauth2.datatype.AuthorizationCode`. 77 | """ 78 | raise NotImplementedError 79 | 80 | def delete_code(self, code): 81 | """ 82 | Deletes an authorization code after it's use per section 4.1.2. 83 | 84 | http://tools.ietf.org/html/rfc6749#section-4.1.2 85 | 86 | :param code: The authorization code. 87 | """ 88 | raise NotImplementedError 89 | 90 | 91 | class ClientStore(object): 92 | """ 93 | Base class for handling OAuth2 clients. 94 | """ 95 | def fetch_by_client_id(self, client_id): 96 | """ 97 | Retrieve a client by its identifier. 98 | 99 | :param client_id: Identifier of a client app. 100 | :return: An instance of :class:`oauth2.datatype.Client`. 101 | :raises: :class:`oauth2.error.ClientNotFoundError` if no data could be retrieved for given client_id. 102 | """ 103 | raise NotImplementedError 104 | -------------------------------------------------------------------------------- /oauth2/store/dynamodb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from oauth2.datatype import AccessToken 4 | from oauth2.error import AccessTokenNotFound 5 | from oauth2.store import AccessTokenStore 6 | 7 | 8 | class DynamodbStore(object): 9 | """ 10 | Uses dynamodb to store access tokens and auth tokens. 11 | 12 | This Store supports ``dynamodb``. Arguments are passed to the 13 | underlying client implementation. For connect to dynamodb use python boto library. 14 | http://boto.cloudhackers.com/en/latest/dynamodb_tut.html 15 | 16 | Initialization:: 17 | 18 | from oauth2.store.dynamodb import TokenStore 19 | # oauth_token = Table('oauth_access_token') 20 | oauth_token = Table.create('users', 21 | schema=[HashKey('token_key')], 22 | global_indexes=[ 23 | GlobalAllIndex('RefreshToken-index', parts=[HashKey('refresh_token')]) 24 | ]) 25 | 26 | token_store = TokenStore(oauth_token) 27 | """ 28 | def __init__(self, connect): 29 | self.connect = connect 30 | 31 | 32 | class TokenStore(AccessTokenStore, DynamodbStore): 33 | """Dynamodb Access Token Store""" 34 | 35 | def save_token(self, access_token): 36 | """ 37 | Stores the access token and additional data in redis. 38 | See :class:`oauth2.store.AccessTokenStore`. 39 | """ 40 | unique_token_key = self._unique_token_key(access_token.client_id, access_token.grant_type, access_token.user_id) 41 | 42 | storing_unique_token = access_token.__dict__ 43 | storing_unique_token.update({'token_key': unique_token_key}) 44 | self.connect.put_item(**storing_unique_token) 45 | 46 | def delete_refresh_token(self, refresh_token): 47 | """ 48 | Deletes a refresh token after use 49 | 50 | :param refresh_token: The refresh token to delete. 51 | """ 52 | access_token = self.fetch_by_refresh_token(refresh_token) 53 | self.connect.delete({'token_key': access_token.token}) 54 | 55 | def fetch_by_refresh_token(self, refresh_token): 56 | """ 57 | Find oauth tokens by refresh token. 58 | """ 59 | tokens_data = self.connect.query2(refresh_token__eq=refresh_token, 60 | index='RefreshToken-index', limit=1) 61 | tokens_res = list(tokens_data) 62 | if not tokens_res: 63 | raise error.AccessTokenNotFound 64 | token_data = tokens_res.pop() 65 | if token_data is None: 66 | raise error.AccessTokenNotFound 67 | data = token_data._data.get('token') # pylint: disable=protected-access 68 | del data['token_key'] 69 | return datatype.AccessToken(**data) 70 | 71 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 72 | """ 73 | Find oauth access token by client_id, grant_type, user_id. 74 | """ 75 | unique_token_key = self._unique_token_key(client_id=client_id, grant_type=grant_type, user_id=user_id) 76 | token_data = self.connect.get_item(token_key=unique_token_key) 77 | if token_data is None: 78 | raise error.AccessTokenNotFound 79 | return datatype.AccessToken(**token_data) 80 | 81 | @classmethod 82 | def _unique_token_key(cls, client_id, grant_type, user_id): 83 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id) 84 | -------------------------------------------------------------------------------- /oauth2/store/memcache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import memcache 3 | from oauth2.datatype import AccessToken, AuthorizationCode 4 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound 5 | from oauth2.store import AccessTokenStore, AuthCodeStore 6 | 7 | 8 | class TokenStore(AccessTokenStore, AuthCodeStore): 9 | """ 10 | Uses memcache to store access tokens and auth tokens. 11 | 12 | This Store supports ``python-memcached``. Arguments are passed to the 13 | underlying client implementation. 14 | 15 | Initialization by passing an object:: 16 | 17 | # This example uses python-memcached 18 | import memcache 19 | 20 | # Somewhere in your application 21 | mc = memcache.Client(servers=['127.0.0.1:11211'], debug=0) 22 | # ... 23 | token_store = TokenStore(mc=mc) 24 | 25 | Initialization using ``python-memcached``:: 26 | token_store = TokenStore(servers=['127.0.0.1:11211'], debug=0) 27 | """ 28 | def __init__(self, mc=None, prefix="oauth2", *args, **kwargs): 29 | self.prefix = prefix 30 | 31 | if mc is not None: 32 | self.mc = mc 33 | else: 34 | self.mc = memcache.Client(*args, **kwargs) 35 | 36 | def fetch_by_code(self, code): 37 | """ 38 | Returns data belonging to an authorization code from memcache or ``None`` if no data was found. 39 | 40 | See :class:`oauth2.store.AuthCodeStore`. 41 | """ 42 | code_data = self.mc.get(self._generate_cache_key(code)) 43 | 44 | if code_data is None: 45 | raise AuthCodeNotFound 46 | 47 | return AuthorizationCode(**code_data) 48 | 49 | def save_code(self, authorization_code): 50 | """ 51 | Stores the data belonging to an authorization code token in memcache. 52 | 53 | See :class:`oauth2.store.AuthCodeStore`. 54 | """ 55 | key = self._generate_cache_key(authorization_code.code) 56 | 57 | self.mc.set(key, {"client_id": authorization_code.client_id, 58 | "code": authorization_code.code, 59 | "expires_at": authorization_code.expires_at, 60 | "redirect_uri": authorization_code.redirect_uri, 61 | "scopes": authorization_code.scopes, 62 | "data": authorization_code.data, 63 | "user_id": authorization_code.user_id}) 64 | 65 | def delete_code(self, code): 66 | """ 67 | Deletes an authorization code after use 68 | 69 | :param code: The authorization code. 70 | """ 71 | self.mc.delete(self._generate_cache_key(code)) 72 | 73 | def save_token(self, access_token): 74 | """ 75 | Stores the access token and additional data in memcache. 76 | 77 | See :class:`oauth2.store.AccessTokenStore`. 78 | """ 79 | key = self._generate_cache_key(access_token.token) 80 | self.mc.set(key, access_token.__dict__) 81 | 82 | unique_token_key = self._unique_token_key(access_token.client_id, 83 | access_token.grant_type, 84 | access_token.user_id) 85 | self.mc.set(self._generate_cache_key(unique_token_key), access_token.__dict__) 86 | 87 | if access_token.refresh_token is not None: 88 | rft_key = self._generate_cache_key(access_token.refresh_token) 89 | self.mc.set(rft_key, access_token.__dict__) 90 | 91 | def delete_refresh_token(self, refresh_token): 92 | """ 93 | Deletes a refresh token after use 94 | 95 | :param refresh_token: The refresh token to delete. 96 | """ 97 | access_token = self.fetch_by_refresh_token(refresh_token) 98 | self.mc.delete(self._generate_cache_key(access_token.token)) 99 | self.mc.delete(self._generate_cache_key(refresh_token)) 100 | 101 | def fetch_by_refresh_token(self, refresh_token): 102 | token_data = self.mc.get(refresh_token) 103 | 104 | if token_data is None: 105 | raise AccessTokenNotFound 106 | 107 | return AccessToken(**token_data) 108 | 109 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 110 | data = self.mc.get(self._unique_token_key(client_id, grant_type, user_id)) 111 | 112 | if data is None: 113 | raise AccessTokenNotFound 114 | 115 | return AccessToken(**data) 116 | 117 | def _unique_token_key(self, client_id, grant_type, user_id): 118 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id) 119 | 120 | def _generate_cache_key(self, identifier): 121 | return self.prefix + "_" + identifier 122 | -------------------------------------------------------------------------------- /oauth2/store/memory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read or write data from or to local memory. 3 | 4 | Though not very valuable in a production setup, these store adapters are great 5 | for testing purposes. 6 | """ 7 | 8 | from oauth2.datatype import Client 9 | from oauth2.error import (AccessTokenNotFound, AuthCodeNotFound, 10 | ClientNotFoundError) 11 | from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore 12 | 13 | 14 | class ClientStore(ClientStore): 15 | """ 16 | Stores clients in memory. 17 | """ 18 | def __init__(self): 19 | self.clients = {} 20 | 21 | def add_client(self, client_id, client_secret, redirect_uris, 22 | authorized_grants=None, authorized_response_types=None): 23 | """ 24 | Add a client app. 25 | 26 | :param client_id: Identifier of the client app. 27 | :param client_secret: Secret the client app uses for authentication against the OAuth 2.0 provider. 28 | :param redirect_uris: A ``list`` of URIs to redirect to. 29 | """ 30 | self.clients[client_id] = Client( 31 | identifier=client_id, 32 | secret=client_secret, 33 | redirect_uris=redirect_uris, 34 | authorized_grants=authorized_grants, 35 | authorized_response_types=authorized_response_types) 36 | 37 | return True 38 | 39 | def fetch_by_client_id(self, client_id): 40 | """ 41 | Retrieve a client by its identifier. 42 | 43 | :param client_id: Identifier of a client app. 44 | :return: An instance of :class:`oauth2.Client`. 45 | :raises: ClientNotFoundError 46 | """ 47 | if client_id not in self.clients: 48 | raise ClientNotFoundError 49 | 50 | return self.clients[client_id] 51 | 52 | 53 | class TokenStore(AccessTokenStore, AuthCodeStore): 54 | """ 55 | Stores tokens in memory. 56 | 57 | Useful for testing purposes or APIs with a very limited set of clients. 58 | Use memcache or redis as storage to be able to scale. 59 | """ 60 | def __init__(self): 61 | self.access_tokens = {} 62 | self.auth_codes = {} 63 | self.refresh_tokens = {} 64 | self.unique_token_identifier = {} 65 | 66 | def fetch_by_code(self, code): 67 | """ 68 | Returns an AuthorizationCode. 69 | 70 | :param code: The authorization code. 71 | :return: An instance of :class:`oauth2.datatype.AuthorizationCode`. 72 | :raises: :class:`AuthCodeNotFound` if no data could be retrieved for given code. 73 | """ 74 | if code not in self.auth_codes: 75 | raise AuthCodeNotFound 76 | 77 | return self.auth_codes[code] 78 | 79 | def save_code(self, authorization_code): 80 | """ 81 | Stores the data belonging to an authorization code token. 82 | 83 | :param authorization_code: An instance of :class:`oauth2.datatype.AuthorizationCode`. 84 | """ 85 | self.auth_codes[authorization_code.code] = authorization_code 86 | 87 | return True 88 | 89 | def save_token(self, access_token): 90 | """ 91 | Stores an access token and additional data in memory. 92 | 93 | :param access_token: An instance of :class:`oauth2.datatype.AccessToken`. 94 | """ 95 | self.access_tokens[access_token.token] = access_token 96 | 97 | unique_token_key = self._unique_token_key(access_token.client_id, 98 | access_token.grant_type, 99 | access_token.user_id) 100 | 101 | self.unique_token_identifier[unique_token_key] = access_token.token 102 | 103 | if access_token.refresh_token is not None: 104 | self.refresh_tokens[access_token.refresh_token] = access_token 105 | 106 | return True 107 | 108 | def delete_code(self, code): 109 | """ 110 | Deletes an authorization code after use 111 | 112 | :param code: The authorization code. 113 | """ 114 | if code in self.auth_codes: 115 | del self.auth_codes[code] 116 | 117 | def delete_refresh_token(self, refresh_token): 118 | """ 119 | Deletes a refresh token after use 120 | 121 | :param refresh_token: The refresh_token. 122 | """ 123 | if refresh_token in self.refresh_tokens: 124 | del self.refresh_tokens[refresh_token] 125 | 126 | def fetch_by_refresh_token(self, refresh_token): 127 | """ 128 | Find an access token by its refresh token. 129 | 130 | :param refresh_token: The refresh token that was assigned to an ``AccessToken``. 131 | :return: The :class:`oauth2.datatype.AccessToken`. 132 | :raises: :class:`oauth2.error.AccessTokenNotFound` 133 | """ 134 | if refresh_token not in self.refresh_tokens: 135 | raise AccessTokenNotFound 136 | 137 | return self.refresh_tokens[refresh_token] 138 | 139 | def fetch_by_token(self, token): 140 | """ 141 | Returns data associated with an access token or ``None`` if no data was found. 142 | 143 | Useful for cases like validation where the access token needs to be read again. 144 | 145 | :param token: A access token code. 146 | :return: An instance of :class:`oauth2.datatype.AccessToken`. 147 | """ 148 | if token not in self.access_tokens: 149 | raise AccessTokenNotFound 150 | 151 | return self.access_tokens[token] 152 | 153 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 154 | try: 155 | key = self._unique_token_key(client_id, grant_type, user_id) 156 | token = self.unique_token_identifier[key] 157 | except KeyError: 158 | raise AccessTokenNotFound 159 | 160 | return self.fetch_by_token(token) 161 | 162 | def _unique_token_key(self, client_id, grant_type, user_id): 163 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id) 164 | -------------------------------------------------------------------------------- /oauth2/store/mongodb.py: -------------------------------------------------------------------------------- 1 | """ 2 | Store adapters to read/write data to from/to mongodb using pymongo. 3 | """ 4 | 5 | import pymongo 6 | 7 | from oauth2.datatype import AccessToken, AuthorizationCode, Client 8 | from oauth2.error import (AccessTokenNotFound, AuthCodeNotFound, 9 | ClientNotFoundError) 10 | from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore 11 | 12 | 13 | class MongodbStore(object): 14 | """ 15 | Base class extended by all concrete store adapters. 16 | """ 17 | 18 | def __init__(self, collection): 19 | self.collection = collection 20 | 21 | 22 | class AccessTokenStore(AccessTokenStore, MongodbStore): 23 | """ 24 | Create a new instance like this:: 25 | 26 | from pymongo import MongoClient 27 | 28 | client = MongoClient('localhost', 27017) 29 | db = client.test_database 30 | access_token_store = AccessTokenStore(collection=db["access_tokens"]) 31 | """ 32 | 33 | def fetch_by_refresh_token(self, refresh_token): 34 | data = self.collection.find_one({"refresh_token": refresh_token}) 35 | 36 | if data is None: 37 | raise AccessTokenNotFound 38 | 39 | return AccessToken(client_id=data.get("client_id"), 40 | grant_type=data.get("grant_type"), 41 | token=data.get("token"), 42 | data=data.get("data"), 43 | expires_at=data.get("expires_at"), 44 | refresh_token=data.get("refresh_token"), 45 | refresh_expires_at=data.get("refresh_expires_at"), 46 | scopes=data.get("scopes")) 47 | 48 | def delete_refresh_token(self, refresh_token): 49 | """ 50 | Deletes (invalidates) an old refresh token after use 51 | 52 | :param refresh_token: The refresh token. 53 | """ 54 | self.collection.remove({"refresh_token": refresh_token}) 55 | 56 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 57 | data = self.collection.find_one({"client_id": client_id, 58 | "grant_type": grant_type, 59 | "user_id": user_id}, 60 | sort=[("expires_at", 61 | pymongo.DESCENDING)]) 62 | 63 | if data is None: 64 | raise AccessTokenNotFound 65 | 66 | return AccessToken(client_id=data.get("client_id"), 67 | grant_type=data.get("grant_type"), 68 | token=data.get("token"), 69 | data=data.get("data"), 70 | expires_at=data.get("expires_at"), 71 | refresh_token=data.get("refresh_token"), 72 | refresh_expires_at=data.get("refresh_expires_at"), 73 | scopes=data.get("scopes"), 74 | user_id=data.get("user_id")) 75 | 76 | def save_token(self, access_token): 77 | self.collection.insert({ 78 | "client_id": access_token.client_id, 79 | "grant_type": access_token.grant_type, 80 | "token": access_token.token, 81 | "data": access_token.data, 82 | "expires_at": access_token.expires_at, 83 | "refresh_token": access_token.refresh_token, 84 | "refresh_expires_at": access_token.refresh_expires_at, 85 | "scopes": access_token.scopes, 86 | "user_id": access_token.user_id}) 87 | 88 | return True 89 | 90 | 91 | class AuthCodeStore(AuthCodeStore, MongodbStore): 92 | """ 93 | Create a new instance like this:: 94 | 95 | from pymongo import MongoClient 96 | 97 | client = MongoClient('localhost', 27017) 98 | db = client.test_database 99 | access_token_store = AuthCodeStore(collection=db["auth_codes"]) 100 | """ 101 | 102 | def fetch_by_code(self, code): 103 | code_data = self.collection.find_one({"code": code}) 104 | 105 | if code_data is None: 106 | raise AuthCodeNotFound 107 | 108 | return AuthorizationCode(client_id=code_data.get("client_id"), 109 | code=code_data.get("code"), 110 | expires_at=code_data.get("expires_at"), 111 | redirect_uri=code_data.get("redirect_uri"), 112 | scopes=code_data.get("scopes"), 113 | data=code_data.get("data"), 114 | user_id=code_data.get("user_id")) 115 | 116 | def save_code(self, authorization_code): 117 | self.collection.insert({ 118 | "client_id": authorization_code.client_id, 119 | "code": authorization_code.code, 120 | "expires_at": authorization_code.expires_at, 121 | "redirect_uri": authorization_code.redirect_uri, 122 | "scopes": authorization_code.scopes, 123 | "data": authorization_code.data, 124 | "user_id": authorization_code.user_id}) 125 | 126 | return True 127 | 128 | def delete_code(self, code): 129 | """ 130 | Deletes an authorization code after use 131 | 132 | :param code: The authorization code. 133 | """ 134 | self.collection.remove({"code": code}) 135 | 136 | 137 | class ClientStore(ClientStore, MongodbStore): 138 | """ 139 | Create a new instance like this:: 140 | 141 | from pymongo import MongoClient 142 | 143 | client = MongoClient('localhost', 27017) 144 | db = client.test_database 145 | access_token_store = ClientStore(collection=db["clients"]) 146 | """ 147 | 148 | def fetch_by_client_id(self, client_id): 149 | client_data = self.collection.find_one({"identifier": client_id}) 150 | 151 | if client_data is None: 152 | raise ClientNotFoundError 153 | 154 | return Client( 155 | identifier=client_data.get("identifier"), 156 | secret=client_data.get("secret"), 157 | redirect_uris=client_data.get("redirect_uris"), 158 | authorized_grants=client_data.get("authorized_grants"), 159 | authorized_response_types=client_data.get( 160 | "authorized_response_types" 161 | )) 162 | -------------------------------------------------------------------------------- /oauth2/store/redisdb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | import redis 5 | from oauth2.compatibility import json 6 | from oauth2.datatype import AccessToken, AuthorizationCode, Client 7 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, ClientNotFoundError 8 | from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore 9 | 10 | 11 | class RedisStore(object): 12 | """ 13 | Uses redis to store access tokens and auth tokens. 14 | 15 | This Store supports ``redis``. Arguments are passed to the 16 | underlying client implementation. 17 | 18 | Initialization:: 19 | 20 | import redisdb 21 | 22 | token_store = TokenStore(host="127.0.0.1", port=6379, db=0) 23 | """ 24 | def __init__(self, rs=None, prefix="oauth2", *args, **kwargs): 25 | self.prefix = prefix 26 | 27 | if rs is not None: 28 | self.rs = rs 29 | else: 30 | self.rs = redis.StrictRedis(*args, **kwargs) 31 | 32 | def delete(self, name): 33 | cache_key = self._generate_cache_key(name) 34 | self.rs.delete(cache_key) 35 | 36 | def write(self, name, data): 37 | """It makes no sense to hold the key after the expiration time""" 38 | 39 | expires_at = data.get('expires_at') 40 | cache_key = self._generate_cache_key(name) 41 | 42 | if expires_at: 43 | token_ttl = int(expires_at) - int(time.time()) 44 | self.rs.set(cache_key, json.dumps(data), ex=token_ttl) 45 | else: 46 | self.rs.set(cache_key, json.dumps(data)) 47 | 48 | def read(self, name): 49 | cache_key = self._generate_cache_key(name) 50 | data = self.rs.get(cache_key) 51 | 52 | if data is None: 53 | return None 54 | 55 | return json.loads(data.decode("utf-8")) 56 | 57 | def _generate_cache_key(self, identifier): 58 | return self.prefix + "_" + identifier 59 | 60 | 61 | class TokenStore(AccessTokenStore, AuthCodeStore, RedisStore): 62 | def fetch_by_code(self, code): 63 | """ 64 | Returns data belonging to an authorization code from redis or ``None`` if no data was found. 65 | 66 | See :class:`oauth2.store.AuthCodeStore`. 67 | """ 68 | code_data = self.read(code) 69 | 70 | if code_data is None: 71 | raise AuthCodeNotFound 72 | 73 | return AuthorizationCode(**code_data) 74 | 75 | def save_code(self, authorization_code): 76 | """ 77 | Stores the data belonging to an authorization code token in redis. 78 | 79 | See :class:`oauth2.store.AuthCodeStore`. 80 | """ 81 | self.write(authorization_code.code, 82 | {"client_id": authorization_code.client_id, 83 | "code": authorization_code.code, 84 | "expires_at": authorization_code.expires_at, 85 | "redirect_uri": authorization_code.redirect_uri, 86 | "scopes": authorization_code.scopes, 87 | "data": authorization_code.data, 88 | "user_id": authorization_code.user_id}) 89 | 90 | def delete_code(self, code): 91 | """ 92 | Deletes an authorization code after use 93 | 94 | :param code: The authorization code. 95 | """ 96 | self.delete(code) 97 | 98 | def save_token(self, access_token): 99 | """ 100 | Stores the access token and additional data in redis. 101 | 102 | See :class:`oauth2.store.AccessTokenStore`. 103 | """ 104 | self.write(access_token.token, access_token.__dict__) 105 | 106 | unique_token_key = self._unique_token_key(access_token.client_id, access_token.grant_type, access_token.user_id) 107 | self.write(unique_token_key, access_token.__dict__) 108 | 109 | if access_token.refresh_token is not None: 110 | self.write(access_token.refresh_token, access_token.__dict__) 111 | 112 | def delete_refresh_token(self, refresh_token): 113 | """ 114 | Deletes a refresh token after use 115 | 116 | :param refresh_token: The refresh token to delete. 117 | """ 118 | access_token = self.fetch_by_refresh_token(refresh_token) 119 | 120 | self.delete(access_token.token) 121 | 122 | def fetch_by_refresh_token(self, refresh_token): 123 | token_data = self.read(refresh_token) 124 | 125 | if token_data is None: 126 | raise AccessTokenNotFound 127 | 128 | return AccessToken(**token_data) 129 | 130 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 131 | unique_token_key = self._unique_token_key(client_id=client_id, grant_type=grant_type, user_id=user_id) 132 | token_data = self.read(unique_token_key) 133 | 134 | if token_data is None: 135 | raise AccessTokenNotFound 136 | 137 | return AccessToken(**token_data) 138 | 139 | def _unique_token_key(self, client_id, grant_type, user_id): 140 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id) 141 | 142 | 143 | class ClientStore(ClientStore, RedisStore): 144 | def add_client(self, client_id, client_secret, redirect_uris, 145 | authorized_grants=None, authorized_response_types=None): 146 | """ 147 | Add a client app. 148 | 149 | :param client_id: Identifier of the client app. 150 | :param client_secret: Secret the client app uses for authentication against the OAuth 2.0 provider. 151 | :param redirect_uris: A ``list`` of URIs to redirect to. 152 | """ 153 | self.write(client_id, 154 | {"identifier": client_id, 155 | "secret": client_secret, 156 | "redirect_uris": redirect_uris, 157 | "authorized_grants": authorized_grants, 158 | "authorized_response_types": authorized_response_types}) 159 | 160 | return True 161 | 162 | def fetch_by_client_id(self, client_id): 163 | client_data = self.read(client_id) 164 | 165 | if client_data is None: 166 | raise ClientNotFoundError 167 | 168 | return Client(identifier=client_data["identifier"], 169 | secret=client_data["secret"], 170 | redirect_uris=client_data["redirect_uris"], 171 | authorized_grants=client_data["authorized_grants"], 172 | authorized_response_types=client_data["authorized_response_types"]) 173 | -------------------------------------------------------------------------------- /oauth2/store/stateless.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from oauth2.datatype import AccessToken 4 | from oauth2.error import AccessTokenNotFound 5 | from oauth2.store import AccessTokenStore 6 | from oauth2.tokengenerator import StatelessTokenGenerator 7 | 8 | 9 | class TokenStore(AccessTokenStore): 10 | """Uses stateless token to validate access tokens and auth tokens. 11 | 12 | This Dummy store for supports ``stateless``. Arguments are passed to the underlying client implementation. 13 | 14 | Initialization:: 15 | 16 | from oauth2.store.stateless import TokenStore 17 | from oauth2.tokengenerator import StatelessTokenGenerator 18 | 19 | stateless_token = StatelessTokenGenerator(secret_key='xxx') 20 | token_store = TokenStore(stateless_token) 21 | """ 22 | 23 | def __init__(self, stateless_token): 24 | if isinstance(stateless_token, StatelessTokenGenerator) is False: 25 | raise AccessTokenNotFound( 26 | "Token store adapter must inherit from class '{0}'".format(self.site_adapter_class.__name__) 27 | ) 28 | self.stateless_token = stateless_token 29 | 30 | def save_token(self, access_token): 31 | """ 32 | Just dummy interface who imulate store tokens. 33 | See :class:`oauth2.store.AccessTokenStore`. 34 | """ 35 | pass 36 | 37 | def delete_refresh_token(self, refresh_token): 38 | """ 39 | Just dummy interface who imulate delete tokens. 40 | 41 | :param refresh_token: The refresh token to delete. 42 | """ 43 | pass 44 | 45 | def fetch_by_refresh_token(self, refresh_token): 46 | """ 47 | Stateless token can generate oauth new tokens by refresh token. 48 | """ 49 | data = self.stateless_token.validate_token(token, 'refresh_token') 50 | return datatype.AccessToken(**data) 51 | 52 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id): 53 | """ 54 | Stateless implementation can't fitch token. 55 | """ 56 | pass 57 | -------------------------------------------------------------------------------- /oauth2/test/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | -------------------------------------------------------------------------------- /oauth2/test/store/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkanthey/oauth2-stateless/fea3c0a3eca4bf4874f16dcabf2a1b3e9c80cfb0/oauth2/test/store/__init__.py -------------------------------------------------------------------------------- /oauth2/test/store/test_memcache.py: -------------------------------------------------------------------------------- 1 | from mock import Mock, call 2 | 3 | from oauth2.datatype import AccessToken, AuthorizationCode 4 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound 5 | from oauth2.store.memcache import TokenStore 6 | from oauth2.test import unittest 7 | 8 | 9 | class MemcacheTokenStoreTestCase(unittest.TestCase): 10 | def setUp(self): 11 | self.cache_prefix = "test" 12 | 13 | def _generate_test_cache_key(self, key): 14 | return self.cache_prefix + "_" + key 15 | 16 | def test_fetch_by_code(self): 17 | code = "abc" 18 | saved_data = {"client_id": "myclient", "code": code, 19 | "expires_at": 100, "redirect_uri": "http://localhost", 20 | "scopes": ["foo_read", "foo_write"], 21 | "data": {"name": "test"}} 22 | 23 | mc_mock = Mock(spec=["get"]) 24 | mc_mock.get.return_value = saved_data 25 | 26 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix) 27 | 28 | auth_code = store.fetch_by_code(code) 29 | 30 | mc_mock.get.assert_called_with(self._generate_test_cache_key(code)) 31 | self.assertEqual(auth_code.client_id, saved_data["client_id"]) 32 | self.assertEqual(auth_code.code, saved_data["code"]) 33 | self.assertEqual(auth_code.expires_at, saved_data["expires_at"]) 34 | self.assertEqual(auth_code.redirect_uri, saved_data["redirect_uri"]) 35 | self.assertEqual(auth_code.scopes, saved_data["scopes"]) 36 | self.assertEqual(auth_code.data, saved_data["data"]) 37 | 38 | def test_fetch_by_code_no_data(self): 39 | mc_mock = Mock(spec=["get"]) 40 | mc_mock.get.return_value = None 41 | 42 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix) 43 | 44 | with self.assertRaises(AuthCodeNotFound): 45 | store.fetch_by_code("abc") 46 | 47 | def test_save_code(self): 48 | data = {"client_id": "myclient", "code": "abc", "expires_at": 100, 49 | "redirect_uri": "http://localhost", 50 | "scopes": ["foo_read", "foo_write"], 51 | "data": {"name": "test"}, "user_id": 1} 52 | 53 | auth_code = AuthorizationCode(**data) 54 | 55 | cache_key = self._generate_test_cache_key(data["code"]) 56 | 57 | mc_mock = Mock(spec=["set"]) 58 | 59 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix) 60 | 61 | store.save_code(auth_code) 62 | 63 | mc_mock.set.assert_called_with(cache_key, data) 64 | 65 | def test_save_token(self): 66 | data = {"client_id": "myclient", "token": "xyz", 67 | "data": {"name": "test"}, "scopes": ["foo_read", "foo_write"], 68 | "expires_at": None, "refresh_token": "mno", 69 | "refresh_expires_at": None, 70 | "grant_type": "authorization_code", 71 | "user_id": 123} 72 | 73 | access_token = AccessToken(**data) 74 | 75 | cache_key = self._generate_test_cache_key(access_token.token) 76 | refresh_token_key = self._generate_test_cache_key(access_token.refresh_token) 77 | unique_token_key = self._generate_test_cache_key( 78 | "{0}_{1}_{2}".format(access_token.client_id, 79 | access_token.grant_type, 80 | access_token.user_id) 81 | ) 82 | 83 | mc_mock = Mock(spec=["set"]) 84 | 85 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix) 86 | 87 | store.save_token(access_token) 88 | 89 | mc_mock.set.assert_has_calls([call(cache_key, data), 90 | call(unique_token_key, data), 91 | call(refresh_token_key, data)]) 92 | 93 | def test_fetch_existing_token_of_user(self): 94 | data = {"client_id": "myclient", "token": "xyz", 95 | "data": {"name": "test"}, "scopes": ["foo_read", "foo_write"], 96 | "expires_at": None, "refresh_token": "mno", 97 | "grant_type": "authorization_code", 98 | "user_id": 123} 99 | 100 | mc_mock = Mock(spec=["get"]) 101 | mc_mock.get.return_value = data 102 | 103 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix) 104 | 105 | access_token = store.fetch_existing_token_of_user( 106 | client_id="myclient", 107 | grant_type="authorization_code", 108 | user_id=123) 109 | 110 | self.assertTrue(isinstance(access_token, AccessToken)) 111 | 112 | def test_fetch_existing_token_of_user_no_data(self): 113 | mc_mock = Mock(spec=["get"]) 114 | mc_mock.get.return_value = None 115 | 116 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix) 117 | 118 | with self.assertRaises(AccessTokenNotFound): 119 | store.fetch_existing_token_of_user(client_id="myclient", 120 | grant_type="authorization_code", 121 | user_id=123) 122 | -------------------------------------------------------------------------------- /oauth2/test/store/test_memory.py: -------------------------------------------------------------------------------- 1 | from oauth2.datatype import AccessToken, AuthorizationCode 2 | from oauth2.error import AuthCodeNotFound, ClientNotFoundError 3 | from oauth2.store.memory import ClientStore, TokenStore 4 | from oauth2.test import unittest 5 | 6 | 7 | class MemoryClientStoreTestCase(unittest.TestCase): 8 | def test_add_client_and_fetch_by_client_id(self): 9 | expected_client_data = {"client_id": "abc", "client_secret": "xyz", 10 | "redirect_uris": ["http://localhost"]} 11 | 12 | store = ClientStore() 13 | 14 | success = store.add_client(expected_client_data["client_id"], 15 | expected_client_data["client_secret"], 16 | expected_client_data["redirect_uris"]) 17 | self.assertTrue(success) 18 | 19 | client = store.fetch_by_client_id("abc") 20 | 21 | self.assertEqual(client.identifier, expected_client_data["client_id"]) 22 | self.assertEqual(client.secret, expected_client_data["client_secret"]) 23 | self.assertEqual(client.redirect_uris, expected_client_data["redirect_uris"]) 24 | 25 | def test_fetch_by_client_id_no_client(self): 26 | store = ClientStore() 27 | 28 | with self.assertRaises(ClientNotFoundError): 29 | store.fetch_by_client_id("abc") 30 | 31 | class MemoryTokenStoreTestCase(unittest.TestCase): 32 | def setUp(self): 33 | self.access_token_data = {"client_id": "myclient", 34 | "token": "xyz", 35 | "scopes": ["foo_read", "foo_write"], 36 | "data": {"name": "test"}, 37 | "grant_type": "authorization_code"} 38 | self.auth_code = AuthorizationCode("myclient", "abc", 100, 39 | "http://localhost", 40 | ["foo_read", "foo_write"], 41 | {"name": "test"}) 42 | 43 | self.test_store = TokenStore() 44 | 45 | def test_fetch_by_code(self): 46 | with self.assertRaises(AuthCodeNotFound): 47 | self.test_store.fetch_by_code("unknown") 48 | 49 | def test_save_code_and_fetch_by_code(self): 50 | success = self.test_store.save_code(self.auth_code) 51 | self.assertTrue(success) 52 | 53 | result = self.test_store.fetch_by_code(self.auth_code.code) 54 | 55 | self.assertEqual(result, self.auth_code) 56 | 57 | def test_save_token_and_fetch_by_token(self): 58 | access_token = AccessToken(**self.access_token_data) 59 | 60 | success = self.test_store.save_token(access_token) 61 | self.assertTrue(success) 62 | 63 | result = self.test_store.fetch_by_token(access_token.token) 64 | 65 | self.assertEqual(result, access_token) 66 | -------------------------------------------------------------------------------- /oauth2/test/store/test_mongodb.py: -------------------------------------------------------------------------------- 1 | from mock import Mock 2 | 3 | from oauth2.datatype import AccessToken, AuthorizationCode, Client 4 | from oauth2.error import (AccessTokenNotFound, AuthCodeNotFound, 5 | ClientNotFoundError) 6 | from oauth2.store.mongodb import AccessTokenStore, AuthCodeStore, ClientStore 7 | from oauth2.test import unittest 8 | 9 | 10 | class MongodbAccessTokenStoreTestCase(unittest.TestCase): 11 | def setUp(self): 12 | self.access_token_data = {"client_id": "myclient", 13 | "grant_type": "authorization_code", 14 | "token": "xyz", 15 | "scopes": ["foo_read", "foo_write"], 16 | "data": {"name": "test"}, 17 | "expires_at": 1000, 18 | "refresh_token": "abcd", 19 | "refresh_expires_at": 2000, 20 | "user_id": None} 21 | 22 | def test_fetch_by_refresh_token(self): 23 | refresh_token = "abcd" 24 | 25 | self.access_token_data["refresh_token"] = refresh_token 26 | 27 | collection_mock = Mock(spec=["find_one"]) 28 | collection_mock.find_one.return_value = self.access_token_data 29 | 30 | store = AccessTokenStore(collection=collection_mock) 31 | token = store.fetch_by_refresh_token(refresh_token=refresh_token) 32 | 33 | collection_mock.find_one.assert_called_with( 34 | {"refresh_token": refresh_token}) 35 | self.assertTrue(isinstance(token, AccessToken)) 36 | self.assertDictEqual(token.__dict__, self.access_token_data) 37 | 38 | def test_fetch_by_refresh_token_no_data(self): 39 | collection_mock = Mock(spec=["find_one"]) 40 | collection_mock.find_one.return_value = None 41 | 42 | store = AccessTokenStore(collection=collection_mock) 43 | 44 | with self.assertRaises(AccessTokenNotFound): 45 | store.fetch_by_refresh_token(refresh_token="abcd") 46 | 47 | def test_fetch_existing_token_of_user(self): 48 | test_data = {"client_id": "myclient", 49 | "grant_type": "authorization_code", 50 | "token": "xyz", 51 | "scopes": ["foo_read", "foo_write"], 52 | "data": {"name": "test"}, 53 | "expires_at": 1000, 54 | "refresh_token": "abcd", 55 | "refresh_expires_at": 2000, 56 | "user_id": 123} 57 | 58 | collection_mock = Mock(spec=["find_one"]) 59 | collection_mock.find_one.return_value = test_data 60 | 61 | store = AccessTokenStore(collection=collection_mock) 62 | 63 | token = store.fetch_existing_token_of_user(client_id="myclient", 64 | grant_type="authorization_code", 65 | user_id=123) 66 | 67 | self.assertTrue(isinstance(token, AccessToken)) 68 | self.assertDictEqual(token.__dict__, test_data) 69 | collection_mock.find_one.assert_called_with({"client_id": "myclient", 70 | "grant_type": "authorization_code", 71 | "user_id": 123}, 72 | sort=[("expires_at", -1)]) 73 | 74 | def test_fetch_existing_token_of_user_no_data(self): 75 | collection_mock = Mock(spec=["find_one"]) 76 | collection_mock.find_one.return_value = None 77 | 78 | store = AccessTokenStore(collection=collection_mock) 79 | 80 | with self.assertRaises(AccessTokenNotFound): 81 | store.fetch_existing_token_of_user(client_id="myclient", 82 | grant_type="authorization_code", 83 | user_id=123) 84 | 85 | def test_save_token(self): 86 | access_token = AccessToken(**self.access_token_data) 87 | 88 | collection_mock = Mock(spec=["insert"]) 89 | 90 | store = AccessTokenStore(collection=collection_mock) 91 | store.save_token(access_token) 92 | 93 | collection_mock.insert.assert_called_with(self.access_token_data) 94 | 95 | class MongodbAuthCodeStoreTestCase(unittest.TestCase): 96 | def setUp(self): 97 | self.auth_code_data = {"client_id": "myclient", "expires_at": 1000, 98 | "redirect_uri": "https://redirect", 99 | "scopes": ["foo", "bar"], "data": {}, 100 | "user_id": None} 101 | 102 | self.collection_mock = Mock(spec=["find_one", "insert", "remove"]) 103 | 104 | def test_fetch_by_code(self): 105 | code = "abcd" 106 | 107 | self.collection_mock.find_one.return_value = self.auth_code_data 108 | 109 | self.auth_code_data["code"] = "abcd" 110 | 111 | store = AuthCodeStore(collection=self.collection_mock) 112 | auth_code = store.fetch_by_code(code=code) 113 | 114 | self.collection_mock.find_one.assert_called_with({"code": "abcd"}) 115 | self.assertTrue(isinstance(auth_code, AuthorizationCode)) 116 | self.assertDictEqual(auth_code.__dict__, self.auth_code_data) 117 | 118 | def test_fetch_by_code_no_data(self): 119 | self.collection_mock.find_one.return_value = None 120 | 121 | store = AuthCodeStore(collection=self.collection_mock) 122 | 123 | with self.assertRaises(AuthCodeNotFound): 124 | store.fetch_by_code(code="abcd") 125 | 126 | def test_save_code(self): 127 | self.auth_code_data["code"] = "abcd" 128 | 129 | auth_code = AuthorizationCode(**self.auth_code_data) 130 | 131 | store = AuthCodeStore(collection=self.collection_mock) 132 | store.save_code(auth_code) 133 | 134 | self.collection_mock.insert.assert_called_with(self.auth_code_data) 135 | 136 | class MongodbClientStoreTestCase(unittest.TestCase): 137 | def test_fetch_by_client_id(self): 138 | client_data = {"identifier": "testclient", "secret": "k#4g6", 139 | "redirect_uris": ["https://redirect"], 140 | "authorized_grants": []} 141 | 142 | collection_mock = Mock(spec=["find_one"]) 143 | collection_mock.find_one.return_value = client_data 144 | 145 | store = ClientStore(collection=collection_mock) 146 | client = store.fetch_by_client_id(client_id=client_data["identifier"]) 147 | 148 | collection_mock.find_one.assert_called_with({ 149 | "identifier": client_data["identifier"]}) 150 | self.assertTrue(isinstance(client, Client)) 151 | self.assertEqual(client.identifier, client_data["identifier"]) 152 | 153 | def test_fetch_by_client_id_no_data(self): 154 | collection_mock = Mock(spec=["find_one"]) 155 | collection_mock.find_one.return_value = None 156 | 157 | store = ClientStore(collection=collection_mock) 158 | 159 | with self.assertRaises(ClientNotFoundError): 160 | store.fetch_by_client_id(client_id="testclient") 161 | -------------------------------------------------------------------------------- /oauth2/test/store/test_redisdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from mock import Mock 5 | from oauth2.compatibility import json 6 | from oauth2.datatype import AccessToken 7 | from oauth2.store.redisdb import TokenStore 8 | from oauth2.test import unittest 9 | 10 | 11 | class TokenStoreTestCase(unittest.TestCase): 12 | def test_delete_refresh_token(self): 13 | refresh_token_id = "def" 14 | access_token = AccessToken(client_id="abc", grant_type="token", token="xyz") 15 | 16 | redisdb_mock = Mock(spec=["delete", "get"]) 17 | redisdb_mock.get.return_value = bytes(json.dumps(access_token.__dict__).encode('utf-8')) 18 | 19 | store = TokenStore(rs=redisdb_mock) 20 | store.delete_refresh_token(refresh_token_id) 21 | 22 | self.assertEqual(1, redisdb_mock.delete.call_count) 23 | -------------------------------------------------------------------------------- /oauth2/test/test_client_authenticator.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from base64 import b64encode 3 | 4 | from mock import Mock 5 | from oauth2.client_authenticator import (ClientAuthenticator, http_basic_auth, 6 | request_body) 7 | from oauth2.datatype import Client 8 | from oauth2.error import (ClientNotFoundError, OAuthInvalidError, 9 | OAuthInvalidNoRedirectError) 10 | from oauth2.store import ClientStore 11 | from oauth2.test import unittest 12 | from oauth2.web.wsgi import Request 13 | 14 | 15 | class ClientAuthenticatorTestCase(unittest.TestCase): 16 | def setUp(self): 17 | self.client = Client(identifier="abc", secret="xyz", 18 | authorized_grants=["authorization_code"], 19 | authorized_response_types=["code"], 20 | redirect_uris=["http://callback"]) 21 | self.client_store_mock = Mock(spec=ClientStore) 22 | 23 | self.source_mock = Mock() 24 | 25 | self.authenticator = ClientAuthenticator( 26 | client_store=self.client_store_mock, 27 | source=self.source_mock) 28 | 29 | def test_by_identifier(self): 30 | redirect_uri = "http://callback" 31 | 32 | self.client_store_mock.fetch_by_client_id.return_value = self.client 33 | 34 | request_mock = Mock(spec=Request) 35 | request_mock.get_param.side_effect = [self.client.identifier, redirect_uri] 36 | 37 | client = self.authenticator.by_identifier(request=request_mock) 38 | 39 | self.client_store_mock.fetch_by_client_id.assert_called_with(self.client.identifier) 40 | self.assertEqual(client.redirect_uri, redirect_uri) 41 | 42 | def test_by_identifier_client_id_not_set(self): 43 | request_mock = Mock(spec=Request) 44 | request_mock.get_param.return_value = None 45 | 46 | with self.assertRaises(OAuthInvalidNoRedirectError) as expected: 47 | self.authenticator.by_identifier(request=request_mock) 48 | 49 | self.assertEqual(expected.exception.error, "missing_client_id") 50 | 51 | def test_by_identifier_unknown_client(self): 52 | request_mock = Mock(spec=Request) 53 | request_mock.get_param.return_value = "def" 54 | 55 | self.client_store_mock.fetch_by_client_id.side_effect = ClientNotFoundError 56 | 57 | with self.assertRaises(OAuthInvalidNoRedirectError) as expected: 58 | self.authenticator.by_identifier(request=request_mock) 59 | 60 | self.assertEqual(expected.exception.error, "unknown_client") 61 | 62 | def test_by_identifier_unknown_redirect_uri(self): 63 | response_type = "code" 64 | unknown_redirect_uri = "http://unknown.com" 65 | 66 | request_mock = Mock(spec=Request) 67 | request_mock.get_param.side_effect = [self.client.identifier, 68 | response_type, 69 | unknown_redirect_uri] 70 | 71 | self.client_store_mock.fetch_by_client_id.return_value = self.client 72 | 73 | with self.assertRaises(OAuthInvalidNoRedirectError) as expected: 74 | self.authenticator.by_identifier(request=request_mock) 75 | 76 | self.assertEqual(expected.exception.error, "invalid_redirect_uri") 77 | 78 | def test_by_identifier_secret(self): 79 | client_id = "abc" 80 | client_secret = "xyz" 81 | grant_type = "authorization_code" 82 | 83 | request_mock = Mock(spec=Request) 84 | request_mock.post_param.return_value = grant_type 85 | 86 | self.source_mock.return_value = (client_id, client_secret) 87 | 88 | self.client_store_mock.fetch_by_client_id.return_value = self.client 89 | 90 | self.authenticator.by_identifier_secret(request=request_mock) 91 | self.client_store_mock.fetch_by_client_id.assert_called_with(client_id) 92 | 93 | def test_by_identifier_secret_unknown_client(self): 94 | client_id = "def" 95 | client_secret = "uvw" 96 | 97 | self.source_mock.return_value = (client_id, client_secret) 98 | 99 | request_mock = Mock(spec=Request) 100 | 101 | self.client_store_mock.fetch_by_client_id.side_effect = ClientNotFoundError 102 | 103 | with self.assertRaises(OAuthInvalidError) as expected: 104 | self.authenticator.by_identifier_secret(request_mock) 105 | 106 | self.assertEqual(expected.exception.error, "invalid_client") 107 | 108 | def test_by_identifier_secret_client_not_authorized(self): 109 | client_id = "abc" 110 | client_secret = "xyz" 111 | grant_type = "client_credentials" 112 | 113 | self.source_mock.return_value = (client_id, client_secret) 114 | 115 | request_mock = Mock(spec=Request) 116 | request_mock.post_param.return_value = grant_type 117 | 118 | self.client_store_mock.fetch_by_client_id.return_value = self.client 119 | 120 | with self.assertRaises(OAuthInvalidError) as expected: 121 | self.authenticator.by_identifier_secret(request_mock) 122 | 123 | self.assertEqual(expected.exception.error, "unauthorized_client") 124 | 125 | def test_by_identifier_secret_wrong_secret(self): 126 | client_id = "abc" 127 | client_secret = "uvw" 128 | grant_type = "authorization_code" 129 | 130 | self.source_mock.return_value = (client_id, client_secret) 131 | 132 | request_mock = Mock(spec=Request) 133 | request_mock.post_param.return_value = grant_type 134 | 135 | self.client_store_mock.fetch_by_client_id.return_value = self.client 136 | 137 | with self.assertRaises(OAuthInvalidError) as expected: 138 | self.authenticator.by_identifier_secret(request_mock) 139 | 140 | self.assertEqual(expected.exception.error, "invalid_client") 141 | 142 | 143 | class RequestBodyTestCase(unittest.TestCase): 144 | def test_valid(self): 145 | client_id = "abc" 146 | client_secret = "secret" 147 | 148 | request_mock = Mock(spec=Request) 149 | request_mock.post_param.side_effect = [client_id, client_secret] 150 | 151 | result = request_body(request_mock) 152 | 153 | self.assertEqual(result[0], client_id) 154 | self.assertEqual(result[1], client_secret) 155 | 156 | def test_no_client_id(self): 157 | request_mock = Mock(spec=Request) 158 | request_mock.post_param.return_value = None 159 | 160 | with self.assertRaises(OAuthInvalidError) as expected: 161 | request_body(request_mock) 162 | 163 | self.assertEqual(expected.exception.error, "invalid_request") 164 | 165 | def test_no_client_secret(self): 166 | request_mock = Mock(spec=Request) 167 | request_mock.post_param.side_effect = ["abc", None] 168 | 169 | with self.assertRaises(OAuthInvalidError) as expected: 170 | request_body(request_mock) 171 | 172 | self.assertEqual(expected.exception.error, "invalid_request") 173 | 174 | 175 | class HttpBasicAuthTestCase(unittest.TestCase): 176 | def test_valid(self): 177 | client_id = "testclient" 178 | client_secret = "secret" 179 | 180 | credentials = "{0}:{1}".format(client_id, client_secret) 181 | 182 | encoded = b64encode(credentials.encode("latin1")) 183 | 184 | request_mock = Mock(spec=Request) 185 | request_mock.header.return_value = "Basic {0}".format(encoded.decode("latin1")) 186 | 187 | result_client_id, result_client_secret = http_basic_auth(request=request_mock) 188 | 189 | request_mock.header.assert_called_with("authorization") 190 | 191 | self.assertEqual(result_client_id, client_id) 192 | self.assertEqual(result_client_secret, client_secret) 193 | 194 | def test_header_not_present(self): 195 | request_mock = Mock(spec=Request) 196 | request_mock.header.return_value = None 197 | 198 | with self.assertRaises(OAuthInvalidError) as expected: 199 | http_basic_auth(request=request_mock) 200 | 201 | self.assertEqual(expected.exception.error, "invalid_request") 202 | 203 | def test_invalid_authorization_header(self): 204 | request_mock = Mock(spec=Request) 205 | request_mock.header.return_value = "some-data" 206 | 207 | with self.assertRaises(OAuthInvalidError) as expected: 208 | http_basic_auth(request=request_mock) 209 | 210 | self.assertEqual(expected.exception.error, "invalid_request") 211 | -------------------------------------------------------------------------------- /oauth2/test/test_datatype.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | 3 | from oauth2.datatype import AccessToken, Client 4 | from oauth2.error import RedirectUriUnknown 5 | from oauth2.test import unittest 6 | 7 | 8 | def mock_time(): 9 | return 1000 10 | 11 | 12 | class AccessTokenTestCase(unittest.TestCase): 13 | @patch("time.time", mock_time) 14 | def test_expires_in_expired(self): 15 | access_token = AccessToken(client_id="abc", 16 | grant_type="client_credentials", 17 | token="def", expires_at=999) 18 | 19 | self.assertEqual(access_token.expires_in, 0) 20 | 21 | @patch("time.time", mock_time) 22 | def test_expires_in_not_expired(self): 23 | access_token = AccessToken(client_id="abc", 24 | grant_type="client_credentials", 25 | token="def", expires_at=1100) 26 | 27 | self.assertEqual(access_token.expires_in, 100) 28 | 29 | def test_is_expired_expired_at_not_set(self): 30 | access_token = AccessToken(client_id="abc", 31 | grant_type="client_credentials", 32 | token="def") 33 | 34 | self.assertFalse(access_token.is_expired()) 35 | 36 | 37 | class ClientTestCase(unittest.TestCase): 38 | def test_redirect_uri(self): 39 | client = Client(identifier="abc", secret="xyz", 40 | redirect_uris=["http://callback"]) 41 | 42 | self.assertEqual(client.redirect_uri, "http://callback") 43 | client.redirect_uri = "http://callback" 44 | self.assertEqual(client.redirect_uri, "http://callback") 45 | 46 | with self.assertRaises(RedirectUriUnknown): 47 | client.redirect_uri = "http://another.callback" 48 | 49 | def test_response_type_supported(self): 50 | client = Client(identifier="abc", secret="xyz", 51 | authorized_grants=["test_grant"]) 52 | 53 | self.assertTrue(client.grant_type_supported("test_grant")) 54 | self.assertFalse(client.grant_type_supported("unknown_grant")) 55 | 56 | def test_response_type_supported(self): 57 | client = Client(identifier="abc", secret="xyz", 58 | authorized_response_types=["test_response_type"]) 59 | 60 | self.assertTrue(client.response_type_supported("test_response_type")) 61 | self.assertFalse(client.response_type_supported("unknown")) 62 | -------------------------------------------------------------------------------- /oauth2/test/test_oauth2.py: -------------------------------------------------------------------------------- 1 | from mock import Mock 2 | from oauth2 import Provider 3 | from oauth2.compatibility import json 4 | from oauth2.error import OAuthInvalidError, OAuthInvalidNoRedirectError 5 | from oauth2.grant import (AuthorizationCodeGrant, GrantHandler, RefreshToken, 6 | ResourceOwnerGrant) 7 | from oauth2.store import ClientStore 8 | from oauth2.test import unittest 9 | from oauth2.web import (AuthorizationCodeGrantSiteAdapter, 10 | ResourceOwnerGrantSiteAdapter, Response) 11 | from oauth2.web.wsgi import Request 12 | 13 | 14 | class ProviderTestCase(unittest.TestCase): 15 | def setUp(self): 16 | self.client_store_mock = Mock(spec=ClientStore) 17 | self.token_generator_mock = Mock() 18 | 19 | self.response_mock = Mock(spec=Response) 20 | self.response_mock.body = "" 21 | response_class_mock = Mock(return_value=self.response_mock) 22 | 23 | self.token_generator_mock.expires_in = {} 24 | self.token_generator_mock.refresh_expires_in = 0 25 | 26 | self.auth_server = Provider(access_token_store=Mock(), 27 | auth_code_store=Mock(), 28 | client_store=self.client_store_mock, 29 | token_generator=self.token_generator_mock, 30 | response_class=response_class_mock) 31 | 32 | def test_add_grant_set_expire_time(self): 33 | """ 34 | Provider.add_grant() should set the expiration time on the instance of TokenGenerator 35 | """ 36 | self.auth_server.add_grant( 37 | AuthorizationCodeGrant(expires_in=400, 38 | site_adapter=Mock(spec=AuthorizationCodeGrantSiteAdapter)) 39 | ) 40 | self.auth_server.add_grant( 41 | ResourceOwnerGrant(expires_in=500, 42 | site_adapter=Mock(spec=ResourceOwnerGrantSiteAdapter)) 43 | ) 44 | self.auth_server.add_grant(RefreshToken(expires_in=1200)) 45 | 46 | self.assertEqual(self.token_generator_mock.expires_in[AuthorizationCodeGrant.grant_type], 400) 47 | self.assertEqual(self.token_generator_mock.expires_in[ResourceOwnerGrant.grant_type], 500) 48 | self.assertEqual(self.token_generator_mock.refresh_expires_in, 1200) 49 | 50 | def test_add_grant_set_unexpired_refresh_time(self): 51 | """ 52 | Provider.add_grant() should set the expiration time on the instance of TokenGenerator 53 | """ 54 | self.auth_server.add_grant( 55 | ResourceOwnerGrant(expires_in=0, 56 | site_adapter=Mock(spec=ResourceOwnerGrantSiteAdapter)) 57 | ) 58 | self.auth_server.add_grant(RefreshToken(expires_in=0)) 59 | 60 | self.assertEqual(self.token_generator_mock.expires_in[ResourceOwnerGrant.grant_type], 0) 61 | self.assertEqual(self.token_generator_mock.refresh_expires_in, 0) 62 | 63 | 64 | def test_dispatch(self): 65 | environ = {"session": "data"} 66 | process_result = "response" 67 | 68 | request_mock = Mock(spec=Request) 69 | 70 | grant_handler_mock = Mock(spec=["process", "read_validate_params"]) 71 | grant_handler_mock.process.return_value = process_result 72 | 73 | grant_factory_mock = Mock(return_value=grant_handler_mock) 74 | 75 | self.auth_server.site_adapter = Mock(spec=AuthorizationCodeGrantSiteAdapter) 76 | 77 | self.auth_server.add_grant(grant_factory_mock) 78 | result = self.auth_server.dispatch(request_mock, environ) 79 | 80 | grant_factory_mock.assert_called_with(request_mock, self.auth_server) 81 | grant_handler_mock.read_validate_params.assert_called_with(request_mock) 82 | grant_handler_mock.process.assert_called_with(request_mock, self.response_mock, environ) 83 | self.assertEqual(result, process_result) 84 | 85 | def test_dispatch_no_grant_type_found(self): 86 | error_body = { 87 | "error": "unsupported_response_type", 88 | "error_description": "Grant not supported" 89 | } 90 | 91 | request_mock = Mock(spec=Request) 92 | 93 | result = self.auth_server.dispatch(request_mock, {}) 94 | 95 | self.response_mock.add_header.assert_called_with("Content-Type", "application/json") 96 | self.assertEqual(self.response_mock.status_code, 400) 97 | self.assertEqual(self.response_mock.body, json.dumps(error_body)) 98 | self.assertEqual(result, self.response_mock) 99 | 100 | def test_dispatch_no_client_found(self): 101 | error_body = { 102 | "error": "invalid_redirect_uri", 103 | "error_description": "Invalid redirect URI" 104 | } 105 | 106 | request_mock = Mock(spec=Request) 107 | 108 | grant_handler_mock = Mock(spec=GrantHandler) 109 | grant_handler_mock.process.side_effect = OAuthInvalidNoRedirectError(error="") 110 | 111 | grant_factory_mock = Mock(return_value=grant_handler_mock) 112 | 113 | self.auth_server.add_grant(grant_factory_mock) 114 | result = self.auth_server.dispatch(request_mock, {}) 115 | 116 | self.response_mock.add_header.assert_called_with("Content-Type", "application/json") 117 | self.assertEqual(self.response_mock.status_code, 400) 118 | self.assertEqual(self.response_mock.body, json.dumps(error_body)) 119 | 120 | self.assertEqual(result, self.response_mock) 121 | 122 | def test_dispatch_general_exception(self): 123 | request_mock = Mock(spec=Request) 124 | 125 | grant_handler_mock = Mock(spec=GrantHandler) 126 | grant_handler_mock.process.side_effect = KeyError 127 | 128 | grant_factory_mock = Mock(return_value=grant_handler_mock) 129 | 130 | self.auth_server.add_grant(grant_factory_mock) 131 | self.auth_server.dispatch(request_mock, {}) 132 | 133 | self.assertTrue(grant_handler_mock.handle_error.called) 134 | -------------------------------------------------------------------------------- /oauth2/test/test_tokengenerator.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from oauth2.test import unittest 4 | from oauth2.tokengenerator import URandomTokenGenerator, Uuid4TokenGenerator, StatelessTokenGenerator 5 | 6 | 7 | class StatelessTokenGeneratorTestCase(unittest.TestCase): 8 | def setUp(self): 9 | self.sekret_key = "xxx" 10 | self.token_len_with_grant_type = 100 11 | self.refresh_token_len_with_grant_type = 102 12 | 13 | def test_create_code_token_data_no_expiration(self): 14 | generator = StatelessTokenGenerator(self.sekret_key) 15 | 16 | result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 17 | user_id=None, client_id=None) 18 | 19 | self.assertEqual(result["token_type"], "Bearer") 20 | 21 | def test_check_access_token_for_diff_secret_key(self): 22 | generator = StatelessTokenGenerator(self.sekret_key) 23 | generator1 = StatelessTokenGenerator('yyy') 24 | 25 | result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 26 | user_id='user1', client_id=None) 27 | 28 | result1 = generator1.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 29 | user_id='user1', client_id=None) 30 | 31 | self.assertEqual(result["token_type"], "Bearer") 32 | self.assertEqual(result1["token_type"], "Bearer") 33 | 34 | self.assertNotEqual(result["access_token"], result1["access_token"]) 35 | 36 | def test_create_code_token_data_with_expiration(self): 37 | generator = StatelessTokenGenerator(self.sekret_key) 38 | 39 | generator.expires_in = {'test_grant_type': 600} 40 | 41 | result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 42 | user_id=None, client_id=None) 43 | 44 | self.assertEqual(result["token_type"], "Bearer") 45 | self.assertEqual(len(result["refresh_token"]), self.refresh_token_len_with_grant_type) 46 | self.assertEqual(result["expires_in"], 600) 47 | 48 | def test_check_code_and_access_token(self): 49 | generator = StatelessTokenGenerator(self.sekret_key) 50 | user1 = 'user1' 51 | 52 | code_result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 53 | user_id=None, client_id=None) 54 | 55 | code_result1 = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 56 | user_id=None, client_id=None) 57 | 58 | token_result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 59 | user_id=user1, client_id=None) 60 | 61 | token_result1 = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 62 | user_id=user1, client_id=None) 63 | 64 | self.assertEqual(code_result["token_type"], "Bearer") 65 | self.assertEqual(code_result1["token_type"], "Bearer") 66 | self.assertEqual(token_result["token_type"], "Bearer") 67 | self.assertEqual(token_result1["token_type"], "Bearer") 68 | 69 | # Access_token, Code result shold be diff 70 | self.assertNotEqual(code_result["access_token"], code_result1["access_token"]) 71 | self.assertNotEqual(code_result["access_token"], token_result["access_token"]) 72 | self.assertEqual(token_result["access_token"], token_result1["access_token"]) 73 | 74 | # Unserialize result for access_token shold be the same 75 | result_token = generator.unserialize(token_result["access_token"]) 76 | result1_token = generator.unserialize(token_result1["access_token"]) 77 | self.assertEqual(result_token["user_id"], result1_token["user_id"]) 78 | 79 | def test_create_code_token_data_with_expiration_scopes(self): 80 | generator = StatelessTokenGenerator(self.sekret_key) 81 | 82 | generator.expires_in = {'test_grant_type': 600} 83 | 84 | result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 85 | user_id=None, client_id=None) 86 | 87 | self.assertEqual(result["token_type"], "Bearer") 88 | self.assertEqual(len(result["refresh_token"]), self.refresh_token_len_with_grant_type) 89 | self.assertEqual(result["expires_in"], 600) 90 | 91 | def test_check_stateless_token(self): 92 | generator = StatelessTokenGenerator(self.sekret_key) 93 | 94 | generator.expires_in = {'test_grant_type': 600} 95 | scopes1 = ['xxx', 'yyy'] 96 | user1 = 'user1' 97 | client1 = 'client1' 98 | 99 | result = generator.create_access_token_data(data=None, scopes=scopes1, grant_type='test_grant_type', 100 | user_id=user1, client_id=client1) 101 | 102 | self.assertEqual(result["token_type"], "Bearer") 103 | self.assertEqual(result["expires_in"], 600) 104 | 105 | access_token = result["access_token"] 106 | refresh_token = result["refresh_token"] 107 | 108 | data = generator.unserialize(access_token) 109 | self.assertEqual(data["user_id"], user1) 110 | self.assertEqual(data["client_id"], client1) 111 | self.assertEqual(data["scopes"], scopes1) 112 | self.assertEqual(data["type"], 'access_token') 113 | 114 | refresh_data = generator.unserialize(refresh_token) 115 | self.assertEqual(refresh_data["user_id"], user1) 116 | self.assertEqual(refresh_data["client_id"], client1) 117 | self.assertEqual(refresh_data["scopes"], scopes1) 118 | self.assertEqual(refresh_data["type"], 'refresh_token') 119 | 120 | 121 | class URandomTokenGeneratorTestCase(unittest.TestCase): 122 | def test_generate(self): 123 | length = 20 124 | 125 | generator = URandomTokenGenerator(length=length) 126 | 127 | result = generator.generate() 128 | 129 | self.assertTrue(isinstance(result, str)) 130 | self.assertEqual(len(result), length) 131 | 132 | 133 | class Uuid4TokenGeneratorTestCase(unittest.TestCase): 134 | def setUp(self): 135 | self.uuid_regex = r"^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}-[a-z0-9]{12}$" 136 | 137 | def test_create_access_token_data_no_expiration(self): 138 | generator = Uuid4TokenGenerator() 139 | 140 | result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 141 | user_id=None, client_id=None) 142 | 143 | self.assertRegex(result["access_token"], self.uuid_regex) 144 | self.assertEqual(result["token_type"], "Bearer") 145 | 146 | def test_create_access_token_data_with_expiration(self): 147 | generator = Uuid4TokenGenerator() 148 | 149 | generator.expires_in = {'test_grant_type': 600} 150 | 151 | result = generator.create_access_token_data(data=None, scopes=None, grant_type='test_grant_type', 152 | user_id=None, client_id=None) 153 | 154 | self.assertRegex(result["access_token"], self.uuid_regex) 155 | self.assertEqual(result["token_type"], "Bearer") 156 | self.assertRegex(result["refresh_token"], self.uuid_regex) 157 | self.assertEqual(result["expires_in"], 600) 158 | 159 | def test_generate(self): 160 | generator = Uuid4TokenGenerator() 161 | 162 | result = generator.generate() 163 | regex = re.compile(self.uuid_regex) 164 | match = regex.match(result) 165 | 166 | self.assertEqual(result, match.group()) 167 | 168 | if __name__ == "__main__": 169 | unittest.main() 170 | -------------------------------------------------------------------------------- /oauth2/test/test_web.py: -------------------------------------------------------------------------------- 1 | from mock import Mock 2 | 3 | from oauth2 import Provider 4 | from oauth2.test import unittest 5 | from oauth2.web import Response 6 | from oauth2.web.wsgi import Application, Request 7 | 8 | 9 | class RequestTestCase(unittest.TestCase): 10 | def test_initialization_no_post_data(self): 11 | request_method = "TEST" 12 | query_string = "foo=bar&baz=buz" 13 | 14 | environment = {"REQUEST_METHOD": request_method, 15 | "QUERY_STRING": query_string, 16 | "PATH_INFO": "/"} 17 | 18 | request = Request(environment) 19 | 20 | self.assertEqual(request.method, request_method) 21 | self.assertEqual(request.query_params, {"foo": "bar", "baz": "buz"}) 22 | self.assertEqual(request.query_string, query_string) 23 | self.assertEqual(request.post_params, {}) 24 | 25 | def test_initialization_with_post_data(self): 26 | content_length = "42" 27 | request_method = "POST" 28 | query_string = "" 29 | content = "foo=bar&baz=buz".encode('utf-8') 30 | 31 | wsgi_input_mock = Mock(spec=["read"]) 32 | wsgi_input_mock.read.return_value = content 33 | 34 | environment = {"CONTENT_LENGTH": content_length, 35 | "CONTENT_TYPE": "application/x-www-form-urlencoded", 36 | "REQUEST_METHOD": request_method, 37 | "QUERY_STRING": query_string, 38 | "PATH_INFO": "/", 39 | "wsgi.input": wsgi_input_mock} 40 | 41 | request = Request(environment) 42 | 43 | wsgi_input_mock.read.assert_called_with(int(content_length)) 44 | self.assertEqual(request.method, request_method) 45 | self.assertEqual(request.query_params, {}) 46 | self.assertEqual(request.query_string, query_string) 47 | self.assertEqual(request.post_params, {"foo": "bar", "baz": "buz"}) 48 | 49 | def test_get_param(self): 50 | request_method = "TEST" 51 | query_string = "foo=bar&baz=buz" 52 | 53 | environment = {"REQUEST_METHOD": request_method, 54 | "QUERY_STRING": query_string, 55 | "PATH_INFO": "/a-url"} 56 | 57 | request = Request(environment) 58 | 59 | result = request.get_param("foo") 60 | 61 | self.assertEqual(result, "bar") 62 | 63 | result_default = request.get_param("na") 64 | 65 | self.assertEqual(result_default, None) 66 | 67 | def test_post_param(self): 68 | content_length = "42" 69 | request_method = "POST" 70 | query_string = "" 71 | content = "foo=bar&baz=buz".encode('utf-8') 72 | 73 | wsgi_input_mock = Mock(spec=["read"]) 74 | wsgi_input_mock.read.return_value = content 75 | 76 | environment = {"CONTENT_LENGTH": content_length, 77 | "CONTENT_TYPE": "application/x-www-form-urlencoded", 78 | "REQUEST_METHOD": request_method, 79 | "QUERY_STRING": query_string, 80 | "PATH_INFO": "/", 81 | "wsgi.input": wsgi_input_mock} 82 | 83 | request = Request(environment) 84 | 85 | result = request.post_param("foo") 86 | 87 | self.assertEqual(result, "bar") 88 | 89 | result_default = request.post_param("na") 90 | 91 | self.assertEqual(result_default, None) 92 | 93 | wsgi_input_mock.read.assert_called_with(int(content_length)) 94 | 95 | def test_header(self): 96 | environment = {"REQUEST_METHOD": "GET", 97 | "QUERY_STRING": "", 98 | "PATH_INFO": "/", 99 | "HTTP_AUTHORIZATION": "Basic abcd"} 100 | 101 | request = Request(env=environment) 102 | 103 | self.assertEqual(request.header("authorization"), "Basic abcd") 104 | self.assertIsNone(request.header("unknown")) 105 | self.assertEqual(request.header("unknown", default=0), 0) 106 | 107 | 108 | class ServerTestCase(unittest.TestCase): 109 | def test_call(self): 110 | body = "body" 111 | headers = {"header": "value"} 112 | path = "/authorize" 113 | status_code = 200 114 | http_code = "200 OK" 115 | 116 | environment = {"PATH_INFO": path, "myvar": "value"} 117 | 118 | request_mock = Mock(spec=Request) 119 | request_class_mock = Mock(return_value=request_mock) 120 | 121 | response_mock = Mock(spec=Response) 122 | response_mock.body = body 123 | response_mock.headers = headers 124 | response_mock.status_code = status_code 125 | 126 | provider_mock = Mock(spec=Provider) 127 | provider_mock.dispatch.return_value = response_mock 128 | 129 | start_response_mock = Mock() 130 | 131 | wsgi = Application(provider=provider_mock, authorize_uri=path, 132 | request_class=request_class_mock, 133 | env_vars=["myvar"]) 134 | result = wsgi(environment, start_response_mock) 135 | 136 | request_class_mock.assert_called_with(environment) 137 | provider_mock.dispatch.assert_called_with(request_mock, 138 | {"myvar": "value"}) 139 | start_response_mock.assert_called_with(http_code, 140 | list(headers.items())) 141 | self.assertEqual(result, [body.encode('utf-8')]) 142 | -------------------------------------------------------------------------------- /oauth2/tokengenerator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides various implementations of algorithms to generate an Access Token or Refresh Token. 3 | """ 4 | 5 | import hashlib 6 | import os 7 | import uuid 8 | 9 | import itsdangerous 10 | from oauth2.error import AccessTokenNotFound 11 | 12 | 13 | class TokenGenerator(object): 14 | """ 15 | Base class of every token generator. 16 | """ 17 | 18 | def __init__(self): 19 | """ 20 | Create a new instance of a token generator. 21 | """ 22 | self.expires_in = {} 23 | self.refresh_expires_in = 0 24 | 25 | def create_access_token_data(self, data, scopes, grant_type, user_id, client_id): 26 | """ 27 | Create data needed by an access token. 28 | 29 | :param data: Arbitrary data as returned by the ``authenticate()`` method of a ``SiteAdapter``. 30 | :type data: dict 31 | :param grant_type: 32 | :type grant_type: str 33 | :param user_id: Identifier of the current user as returned by the ``authenticate()`` method of a ``SiteAdapter`` 34 | :type user_id: int 35 | :param client_id: Identifier of the current client. 36 | :type client_id: str 37 | 38 | :return: A ``dict`` containing the ``access_token`` and the 39 | ``token_type``. If the value of ``TokenGenerator.expires_in`` 40 | is larger than 0, a ``refresh_token`` will be generated too. 41 | :rtype: dict 42 | """ 43 | result = {"access_token": self.generate(grant_type, data, scopes, user_id, client_id), "token_type": "Bearer"} 44 | 45 | grant_type_expires_in = self.expires_in.get(grant_type) 46 | if grant_type_expires_in: 47 | result["refresh_token"] = self.refresh_generate(grant_type, data, scopes, user_id, client_id) 48 | result["expires_in"] = grant_type_expires_in 49 | 50 | return result 51 | 52 | def generate(self, grant_type=None, data=None, scopes=None, user_id=None, client_id=None): 53 | """ 54 | Implemented by generators extending this base class. 55 | 56 | :param grant_type: Identifier token grant_type 57 | :type grant_type: str 58 | :param data: Arbitrary data as returned by the ``authenticate()`` method of a ``SiteAdapter``. 59 | :type data: dict 60 | :param scopes: scopes for oauth session 61 | :type scopes: dict 62 | :param user_id: Identifier of the current user as returned by the ``authenticate()`` method of a ``SiteAdapter`` 63 | :type user_id: int 64 | :param client_id: Identifier of the current client. 65 | :type client_id: str 66 | 67 | :raises NotImplementedError: 68 | """ 69 | raise NotImplementedError 70 | 71 | def refresh_generate(self, grant_type=None, data=None, scopes=None, user_id=None, client_id=None): 72 | """ 73 | Implemented by refresh generators extending this base class. 74 | 75 | :param grant_type: Identifier token grant_type 76 | :type grant_type: str 77 | :param data: Arbitrary data as returned by the ``authenticate()`` method of a ``SiteAdapter``. 78 | :type data: dict 79 | :param scopes: scopes for oauth session 80 | :type scopes: dict 81 | :param user_id: Identifier of the current user as returned by the ``authenticate()`` method of a ``SiteAdapter`` 82 | :type user_id: int 83 | :param client_id: Identifier of the current client. 84 | :type client_id: str 85 | 86 | :raises NotImplementedError: 87 | """ 88 | raise NotImplementedError 89 | 90 | 91 | class StatelessTokenGenerator(TokenGenerator): 92 | """ 93 | Generate a token using JSON Web Tokens tokens. 94 | """ 95 | 96 | def __init__(self, secret_key): 97 | self.serializer = itsdangerous.URLSafeTimedSerializer(secret_key) 98 | TokenGenerator.__init__(self) 99 | 100 | def json_serialize(self, data): 101 | _data = dict((k, v) for k, v in data.items() if v) # Remove empty val 102 | return self.serializer.dumps(_data) 103 | 104 | def unserialize(self, serialized): 105 | try: 106 | payload, timestamp = self.serializer.loads(serialized, return_timestamp=True) 107 | payload["refresh_expires_at"] = timestamp 108 | return payload 109 | except (itsdangerous.BadSignature, itsdangerous.SignatureExpired): 110 | raise AccessTokenNotFound 111 | 112 | def validate_token(self, token, token_type): 113 | payload = self.unserialize(token) 114 | if payload['type'] != token_type: 115 | raise AccessTokenNotFound 116 | return payload 117 | 118 | def generate(self, grant_type=None, data=None, scopes=None, user_id=None, client_id=None): 119 | """ 120 | :return: A new token 121 | :rtype: str 122 | """ 123 | # We use the same generator for code and access_token 124 | # JWT will return the same code for different user 125 | user_id = user_id if user_id else str(uuid.uuid4()) 126 | 127 | return self.json_serialize(dict(type='access_token', grant_type=grant_type, user_id=user_id, data=data, 128 | scopes=scopes, client_id=client_id)) 129 | 130 | def refresh_generate(self, grant_type=None, data=None, scopes=None, user_id=None, client_id=None): 131 | """ 132 | :return: A new refresh token 133 | :rtype: str 134 | """ 135 | return self.json_serialize(dict(type='refresh_token', grant_type=grant_type, user_id=user_id, data=data, 136 | scopes=scopes, client_id=client_id)) 137 | 138 | 139 | class URandomTokenGenerator(TokenGenerator): 140 | """ 141 | Create a token using ``os.urandom()``. 142 | """ 143 | 144 | def __init__(self, length=40): 145 | self.token_length = length 146 | TokenGenerator.__init__(self) 147 | 148 | def generate(self, grant_type=None, data=None, scopes=None, user_id=None, client_id=None): 149 | """ 150 | :return: A new token 151 | :rtype: str 152 | """ 153 | random_data = os.urandom(100) 154 | 155 | hash_gen = hashlib.new("sha512") 156 | hash_gen.update(random_data) 157 | 158 | return hash_gen.hexdigest()[:self.token_length] 159 | 160 | refresh_generate = generate 161 | 162 | 163 | class Uuid4TokenGenerator(TokenGenerator): 164 | """ 165 | Generate a token using uuid4. 166 | """ 167 | 168 | def generate(self, grant_type=None, data=None, scopes=None, user_id=None, client_id=None): 169 | """ 170 | :return: A new token 171 | :rtype: str 172 | """ 173 | return str(uuid.uuid4()) 174 | 175 | refresh_generate = generate 176 | -------------------------------------------------------------------------------- /oauth2/web/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class AuthenticatingSiteAdapter(object): 6 | """ 7 | Extended by site adapters that need to authenticate the user. 8 | """ 9 | def authenticate(self, request, environ, scopes, client): 10 | """ 11 | Authenticates a user and checks if she has authorized access. 12 | 13 | :param request: Incoming request data. 14 | :type request: oauth2.web.Request 15 | 16 | :param environ: Environment variables of the request. 17 | :type environ: dict 18 | 19 | :param scopes: A list of strings with each string being one requested scope. 20 | :type scopes: list 21 | 22 | :param client: The client that initiated the authorization process 23 | :type client: oauth2.datatype.Client 24 | 25 | :return: A ``dict`` containing arbitrary data that will be passed to 26 | the current storage adapter and saved with auth code and 27 | access token. Return a tuple in the form 28 | `(additional_data, user_id)` if you want to use 29 | :doc:`unique_token`. 30 | :rtype: dict 31 | 32 | :raises oauth2.error.UserNotAuthenticated: If the user could not be authenticated. 33 | """ 34 | raise NotImplementedError 35 | 36 | 37 | class UserFacingSiteAdapter(object): 38 | """ 39 | Extended by site adapters that need to interact with the user. 40 | 41 | Display HTML or redirect the user agent to another page of your website 42 | where she can do something before being returned to the OAuth 2.0 server. 43 | """ 44 | def render_auth_page(self, request, response, environ, scopes, client): 45 | """ 46 | Defines how to display a confirmation page to the user. 47 | 48 | :param request: Incoming request data. 49 | :type request: oauth2.web.Request 50 | 51 | :param response: Response to return to a client. 52 | :type response: oauth2.web.Response 53 | 54 | :param environ: Environment variables of the request. 55 | :type environ: dict 56 | 57 | :param scopes: A list of strings with each string being one requested scope. 58 | :type scopes: list 59 | 60 | :param client: The client that initiated the authorization process 61 | :type client: oauth2.datatype.Client 62 | 63 | :return: The response passed in as a parameter. It can contain HTML or issue a redirect. 64 | :rtype: oauth2.web.Response 65 | """ 66 | raise NotImplementedError 67 | 68 | def user_has_denied_access(self, request): 69 | """ 70 | Checks if the user has denied access. This will lead to oauth2-stateless 71 | returning a "acess_denied" response to the requesting client app. 72 | 73 | :param request: Incoming request data. 74 | :type request: oauth2.web.Request 75 | 76 | :return: Return ``True`` if the user has denied access. 77 | :rtype: bool 78 | """ 79 | raise NotImplementedError 80 | 81 | 82 | class AuthorizationCodeGrantSiteAdapter(UserFacingSiteAdapter, AuthenticatingSiteAdapter): 83 | """ 84 | Definition of a site adapter as required by 85 | :class:`oauth2.grant.AuthorizationCodeGrant`. 86 | """ 87 | pass 88 | 89 | 90 | class ImplicitGrantSiteAdapter(UserFacingSiteAdapter, AuthenticatingSiteAdapter): 91 | """ 92 | Definition of a site adapter as required by 93 | :class:`oauth2.grant.ImplicitGrant`. 94 | """ 95 | pass 96 | 97 | 98 | class ResourceOwnerGrantSiteAdapter(AuthenticatingSiteAdapter): 99 | """ 100 | Definition of a site adapter as required by 101 | :class:`oauth2.grant.ResourceOwnerGrant`. 102 | """ 103 | pass 104 | 105 | 106 | class Request(object): 107 | """ 108 | Base class defining the interface of a request. 109 | """ 110 | @property 111 | def method(self): 112 | """ 113 | Returns the HTTP method of the request. 114 | """ 115 | raise NotImplementedError 116 | 117 | @property 118 | def path(self): 119 | """ 120 | Returns the current path portion of the current uri. 121 | Used by some grants to determine which action to take. 122 | """ 123 | raise NotImplementedError 124 | 125 | def get_param(self, name, default=None): 126 | """ 127 | Retrieve a parameter from the query string of the request. 128 | """ 129 | raise NotImplementedError 130 | 131 | def header(self, name, default=None): 132 | """ 133 | Retrieve a header of the request. 134 | """ 135 | raise NotImplementedError 136 | 137 | def post_param(self, name, default=None): 138 | """ 139 | Retrieve a parameter from the body of the request. 140 | """ 141 | raise NotImplementedError 142 | 143 | 144 | class Response(object): 145 | """ 146 | Contains data returned to the requesting user agent. 147 | """ 148 | def __init__(self): 149 | self.status_code = 200 150 | self._headers = {"Content-Type": "text/html"} 151 | self.body = "" 152 | 153 | @property 154 | def headers(self): 155 | return self._headers 156 | 157 | def add_header(self, header, value): 158 | """ 159 | Add a header to the response. 160 | """ 161 | self._headers[header] = str(value) 162 | -------------------------------------------------------------------------------- /oauth2/web/aiohttp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | .. warning:: 6 | aiohttp support is currently experimental. 7 | 8 | Use aiohttp to serve token requests: 9 | 10 | .. literalinclude:: examples/aiohttp_server.py 11 | """ 12 | 13 | from __future__ import absolute_import 14 | 15 | from aiohttp import web 16 | 17 | 18 | class Request(object): 19 | """Contains data of the current HTTP request.""" 20 | 21 | def __init__(self, request, data=None): 22 | self.request = request 23 | self.data = data 24 | 25 | @property 26 | def method(self): 27 | return self.request.method 28 | 29 | @property 30 | def path(self): 31 | return self.request.path 32 | 33 | @property 34 | def query_string(self): 35 | return self.request.query_string 36 | 37 | def get_param(self, name, default=None): 38 | return self.request.query.get(name, default) 39 | 40 | def post_param(self, name, default=None): 41 | return self.data.get(name, default) 42 | 43 | def header(self, name, default=None): 44 | return self.request.headers.get(name, default) 45 | 46 | 47 | class OAuth2Handler: 48 | def __init__(self, provider): 49 | """ 50 | :type provider: :class:`oauth2.Provider` 51 | """ 52 | self.provider = provider 53 | 54 | async def dispatch_request(self, request): 55 | response = self.provider.dispatch(request=Request(request), environ=dict()) 56 | return self._map_response(response) 57 | 58 | async def post_dispatch_request(self, request): 59 | data = await request.post() 60 | response = self.provider.dispatch(request=Request(request, data), environ=dict()) 61 | return self._map_response(response) 62 | 63 | def _map_response(self, response): 64 | return web.Response(body=response.body, status=response.status_code, headers=response.headers) 65 | -------------------------------------------------------------------------------- /oauth2/web/flask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Classes for handling flask HTTP request/response flow. 6 | 7 | .. literalinclude:: examples/flask_server.py 8 | """ 9 | 10 | from __future__ import absolute_import 11 | 12 | from functools import wraps 13 | 14 | from flask import request 15 | 16 | 17 | class Request(object): 18 | """Contains data of the current HTTP request.""" 19 | 20 | def __init__(self, request): 21 | self.request = request 22 | 23 | @property 24 | def method(self): 25 | return self.request.method 26 | 27 | @property 28 | def path(self): 29 | return self.request.path 30 | 31 | @property 32 | def query_string(self): 33 | return self.request.query_string.decode('utf-8') 34 | 35 | def get_param(self, name, default=None): 36 | return self.request.args.get(name, default) 37 | 38 | def post_param(self, name, default=None): 39 | if self.header('Content-Type') == 'application/json': 40 | return self.request.json.get(name, default) 41 | return self.request.form.get(name, default) 42 | 43 | def header(self, name, default=None): 44 | return self.request.headers.get(name, default) 45 | 46 | 47 | def oauth_request_hook(provider): 48 | """Initialise Oauth2 interface bewtween flask and oauth2 server""" 49 | 50 | def wrapper(fn): 51 | @wraps(fn) 52 | def decorated_fn(*args, **kwargs): 53 | # We are not call fn(args, kwargs) because oauth.dispatch should doing that. 54 | response = provider.dispatch(Request(request), request.environ) 55 | return response.body, response.status_code, response.headers.items() 56 | return decorated_fn 57 | return wrapper 58 | -------------------------------------------------------------------------------- /oauth2/web/tornado.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | .. warning:: 6 | Tornado support is currently experimental. 7 | 8 | Use Tornado to serve token requests: 9 | 10 | .. literalinclude:: examples/tornado_server.py 11 | """ 12 | 13 | from __future__ import absolute_import 14 | 15 | from tornado.web import RequestHandler 16 | 17 | 18 | class Request(object): 19 | def __init__(self, handler): 20 | """ 21 | :param handler: Handler of the current request 22 | :type handler: :class:`tornado.web.RequestHandler` 23 | """ 24 | self.handler = handler 25 | 26 | @property 27 | def method(self): 28 | return self.handler.request.method 29 | 30 | @property 31 | def path(self): 32 | return self.handler.request.path 33 | 34 | @property 35 | def query_string(self): 36 | return self.handler.request.query 37 | 38 | def get_param(self, name, default=None): 39 | return self.handler.get_query_argument(name=name, default=default) 40 | 41 | def header(self, name, default=None): 42 | return self.handler.request.headers[name] 43 | 44 | def post_param(self, name, default=None): 45 | return self.handler.get_body_argument(name=name, default=default) 46 | 47 | 48 | class OAuth2Handler(RequestHandler): 49 | def initialize(self, provider): 50 | """ 51 | :type provider: :class:`oauth2.Provider` 52 | """ 53 | self.provider = provider 54 | 55 | def get(self): 56 | response = self._dispatch_request() 57 | 58 | self._map_response(response) 59 | 60 | def post(self): 61 | response = self._dispatch_request() 62 | 63 | self._map_response(response) 64 | 65 | def _dispatch_request(self): 66 | return self.provider.dispatch(request=Request(handler=self), environ=dict()) 67 | 68 | def _map_response(self, response): 69 | for name, value in list(response.headers.items()): 70 | self.set_header(name, value) 71 | 72 | self.set_status(response.status_code) 73 | self.write(response.body) 74 | -------------------------------------------------------------------------------- /oauth2/web/wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Classes for handling a HTTP request/response flow. 6 | """ 7 | 8 | from oauth2.compatibility import parse_qs 9 | 10 | 11 | class Request(object): 12 | """ 13 | Contains data of the current HTTP request. 14 | """ 15 | def __init__(self, env): 16 | """ 17 | :param env: Wsgi environment 18 | """ 19 | self.method = env["REQUEST_METHOD"] 20 | self.query_params = {} 21 | self.query_string = env["QUERY_STRING"] 22 | self.path = env["PATH_INFO"] 23 | self.post_params = {} 24 | self.env_raw = env 25 | 26 | for param, value in parse_qs(env["QUERY_STRING"]).items(): 27 | self.query_params[param] = value[0] 28 | 29 | if (self.method == "POST" and env["CONTENT_TYPE"].startswith("application/x-www-form-urlencoded")): 30 | self.post_params = {} 31 | content = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) 32 | post_params = parse_qs(content) 33 | 34 | for param, value in post_params.items(): 35 | decoded_param = param.decode('utf-8') 36 | decoded_value = value[0].decode('utf-8') 37 | self.post_params[decoded_param] = decoded_value 38 | 39 | def get_param(self, name, default=None): 40 | """ 41 | Returns a param of a GET request identified by its name. 42 | """ 43 | try: 44 | return self.query_params[name] 45 | except KeyError: 46 | return default 47 | 48 | def post_param(self, name, default=None): 49 | """ 50 | Returns a param of a POST request identified by its name. 51 | """ 52 | try: 53 | return self.post_params[name] 54 | except KeyError: 55 | return default 56 | 57 | def header(self, name, default=None): 58 | """ 59 | Returns the value of the HTTP header identified by `name`. 60 | """ 61 | wsgi_header = "HTTP_{0}".format(name.upper()) 62 | 63 | try: 64 | return self.env_raw[wsgi_header] 65 | except KeyError: 66 | return default 67 | 68 | 69 | class Application(object): 70 | """ 71 | Implements WSGI. 72 | 73 | .. versionchanged:: 1.0.0 74 | Renamed from ``Server`` to ``Application``. 75 | """ 76 | HTTP_CODES = {200: "200 OK", 77 | 301: "301 Moved Permanently", 78 | 302: "302 Found", 79 | 400: "400 Bad Request", 80 | 401: "401 Unauthorized", 81 | 404: "404 Not Found"} 82 | 83 | def __init__(self, provider, authorize_uri="/authorize", env_vars=None, 84 | request_class=Request, token_uri="/token"): 85 | self.authorize_uri = authorize_uri 86 | self.env_vars = env_vars 87 | self.request_class = request_class 88 | self.provider = provider 89 | self.token_uri = token_uri 90 | 91 | self.provider.authorize_path = authorize_uri 92 | self.provider.token_path = token_uri 93 | 94 | def __call__(self, env, start_response): 95 | environ = {} 96 | 97 | if (env["PATH_INFO"] != self.authorize_uri and env["PATH_INFO"] != self.token_uri): 98 | start_response("404 Not Found", [('Content-type', 'text/html')]) 99 | return [b"Not Found"] 100 | 101 | request = self.request_class(env) 102 | 103 | if isinstance(self.env_vars, list): 104 | for varname in self.env_vars: 105 | if varname in env: 106 | environ[varname] = env[varname] 107 | 108 | response = self.provider.dispatch(request, environ) 109 | start_response(self.HTTP_CODES[response.status_code], list(response.headers.items())) 110 | 111 | return [response.body.encode('utf-8')] 112 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mock 3 | nose 4 | 5 | # Database 6 | pymongo 7 | python-memcached 8 | redis 9 | http://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-1.1.7.tar.gz 10 | 11 | # Web 12 | tornado 13 | flask 14 | aiohttp 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ujson 2 | itsdangerous == 0.24 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [wheel] 5 | universal = 1 6 | 7 | [build_sphinx] 8 | source-dir = docs/ 9 | build-dir = docs/_build 10 | all_files = 1 11 | 12 | [upload_sphinx] 13 | upload-dir = docs/_build/html 14 | 15 | 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name="oauth2-stateless", 7 | version="1.1.2", 8 | description="OAuth 2.0 provider for python with Stateless tokens support", 9 | long_description=open("README.md").read(), 10 | author="Andrew Grytsenko", 11 | author_email="darkanthey@gmail.com", 12 | url="https://github.com/darkanthey/oauth2-stateless", 13 | packages=[ 14 | d[0].replace("/", ".") 15 | for d in os.walk("oauth2") 16 | if not d[0].endswith("__pycache__") 17 | ], 18 | install_requires=["ujson", "itsdangerous"], 19 | extras_require={ 20 | "memcache": ["python-memcached"], 21 | "mongodb": ["pymongo"], 22 | "redis": ["redis"], 23 | }, 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "License :: OSI Approved :: MIT License", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.4", 29 | "Programming Language :: Python :: 3.5", 30 | "Programming Language :: Python :: 3.6", 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | ], 36 | ) 37 | --------------------------------------------------------------------------------