├── .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)
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 | [ ](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------