Compare commits
No commits in common. "master" and "debian" have entirely different histories.
|
@ -1,8 +0,0 @@
|
|||
*.pyc
|
||||
.DS_Store
|
||||
_build
|
||||
.*.sw[po]
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
venv
|
14
.travis.yml
14
.travis.yml
|
@ -1,14 +0,0 @@
|
|||
language: python
|
||||
env:
|
||||
- DJANGO="django>=1.4,<1.5"
|
||||
- DJANGO="django>=1.5,<1.6"
|
||||
- DJANGO="django>=1.6,<1.7"
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
# command to install dependencies
|
||||
install:
|
||||
- pip install -q -r requirements.txt --use-mirrors
|
||||
- pip install -q $DJANGO --use-mirrors -U
|
||||
- python setup.py develop
|
||||
script: ./test.sh
|
22
LICENSE
22
LICENSE
|
@ -1,22 +0,0 @@
|
|||
Copyright (c) 2012 Caffeinehit Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,5 +0,0 @@
|
|||
include LICENSE
|
||||
include README.rst
|
||||
recursive-include provider/templates *.html
|
||||
recursive-include provider/templates *.txt
|
||||
recursive-include provider *json
|
16
README.rst
16
README.rst
|
@ -1,16 +0,0 @@
|
|||
django-oauth2-provider
|
||||
======================
|
||||
|
||||
.. image:: https://travis-ci.org/caffeinehit/django-oauth2-provider.png?branch=master
|
||||
|
||||
*django-oauth2-provider* is a Django application that provides
|
||||
customizable OAuth2\-authentication for your Django projects.
|
||||
|
||||
`Documentation <http://readthedocs.org/docs/django-oauth2-provider/en/latest/>`_
|
||||
|
||||
`Help <https://groups.google.com/d/forum/django-oauth2-provider>`_
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
*django-oauth2-provider* is released under the MIT License. Please see the LICENSE file for details.
|
|
@ -0,0 +1,5 @@
|
|||
django-oauth2-provider (0.2.6-1~eo70+1) wheezy-eobuilder; urgency=low
|
||||
|
||||
* source package automatically created by stdeb 0.6.0+git
|
||||
|
||||
-- Jérôme Schneider <jschneider@entrouvert.com> Wed, 02 Apr 2014 10:47:58 +0200
|
|
@ -0,0 +1 @@
|
|||
7
|
|
@ -0,0 +1,14 @@
|
|||
Source: django-oauth2-provider
|
||||
Maintainer: Jérôme Schneider <jschneider@entrouvert.com>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 7)
|
||||
Standards-Version: 3.9.1
|
||||
|
||||
Package: python-django-oauth2-provider
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python-shortuuid (>= 0.4)
|
||||
Description: Provide OAuth2 access to your app
|
||||
django-oauth2-provider is a Django application that provides
|
||||
customizable OAuth2 authentication for your Django projects.
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --with python2
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
|
@ -0,0 +1 @@
|
|||
extend-diff-ignore="\.egg-info"
|
130
docs/Makefile
130
docs/Makefile
|
@ -1,130 +0,0 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-oauth2-provider.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-oauth2-provider.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-oauth2-provider"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-oauth2-provider"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
make -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
129
docs/api.rst
129
docs/api.rst
|
@ -1,129 +0,0 @@
|
|||
|
||||
`provider`
|
||||
==========
|
||||
|
||||
`provider.constants`
|
||||
--------------------
|
||||
.. automodule:: provider.constants
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
.. currentmodule:: provider.constants
|
||||
|
||||
.. attribute:: RESPONSE_TYPE_CHOICES
|
||||
|
||||
:settings: `OAUTH_RESPONSE_TYPE_CHOICES`
|
||||
|
||||
The response types as outlined by :rfc:`3.1.1`
|
||||
|
||||
.. attribute:: SCOPES
|
||||
|
||||
:settings: `OAUTH_SCOPES`
|
||||
|
||||
A choice of scopes. A detailed implementation is left to the developer.
|
||||
The current default implementation in :attr:`provider.oauth2.scope` makes
|
||||
use of bit shifting operations to combine read and write permissions.
|
||||
|
||||
.. attribute:: EXPIRE_DELTA
|
||||
|
||||
:settings: `OAUTH_EXPIRE_DELTA`
|
||||
:default: `datetime.timedelta(days=365)`
|
||||
|
||||
The time to expiry for access tokens as outlined in :rfc:`4.2.2` and
|
||||
:rfc:`5.1`.
|
||||
|
||||
.. attribute:: EXPIRE_CODE_DELTA
|
||||
|
||||
:settings: `OAUTH_EXPIRE_CODE_DELTA`
|
||||
:default: `datetime.timedelta(seconds=10*60)`
|
||||
|
||||
The time to expiry for an authorization code grant as outlined in :rfc:`4.1.2`.
|
||||
|
||||
.. attribute:: DELETE_EXPIRED
|
||||
|
||||
:settings: `OAUTH_DELETE_EXPIRED`
|
||||
:default: `False`
|
||||
|
||||
To remove expired tokens immediately instead of letting them persist, set
|
||||
to `True`.
|
||||
|
||||
.. attribute:: ENFORCE_SECURE
|
||||
|
||||
:settings: `OAUTH_ENFORCE_SECURE`
|
||||
:default: `False`
|
||||
|
||||
To enforce secure communication on application level, set to `True`.
|
||||
|
||||
.. attribute:: SESSION_KEY
|
||||
|
||||
:settings: `OAUTH_SESSION_KEY`
|
||||
:default: `"oauth"`
|
||||
|
||||
Session key prefix to store temporary data while the user is completing
|
||||
the authentication / authorization process.
|
||||
|
||||
.. attribute:: SINGLE_ACCESS_TOKEN
|
||||
|
||||
:settings: `OAUTH_SINGLE_ACCESS_TOKEN`
|
||||
:default: `False`
|
||||
|
||||
To have the provider only create and retrieve one access token per
|
||||
user/client/scope combination, set to `True`.
|
||||
|
||||
`provider.forms`
|
||||
----------------
|
||||
.. automodule:: provider.forms
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.scope`
|
||||
-----------------------
|
||||
.. automodule:: provider.scope
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.templatetags.scope`
|
||||
-----------------------------
|
||||
.. automodule:: provider.templatetags.scope
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.utils`
|
||||
----------------
|
||||
.. automodule:: provider.utils
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.views`
|
||||
----------------
|
||||
.. automodule:: provider.views
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
|
||||
`provider.oauth2`
|
||||
=================
|
||||
|
||||
`provider.oauth2.forms`
|
||||
-----------------------
|
||||
.. automodule:: provider.oauth2.forms
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.oauth2.models`
|
||||
------------------------
|
||||
.. automodule:: provider.oauth2.models
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.oauth2.urls`
|
||||
----------------------
|
||||
.. automodule:: provider.oauth2.urls
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
|
||||
`provider.oauth2.views`
|
||||
-----------------------
|
||||
.. automodule:: provider.oauth2.views
|
||||
:members:
|
||||
:no-undoc-members:
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
v 0.2
|
||||
-----
|
||||
* *Breaking change* Moved ``provider.oauth2.scope`` to ``provider.scope``
|
||||
* *Breaking change* Replaced the write scope with a new write scope that includes reading
|
||||
* Default scope for new ``provider.oauth2.models.AccessToken`` is now ``provider.constants.SCOPES[0][0]``
|
||||
* Access token response returns a space seperated list of scopes instead of an integer value
|
223
docs/conf.py
223
docs/conf.py
|
@ -1,223 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-oauth2-provider documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Jan 24 15:40:05 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
sys.path = [(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))] + sys.path
|
||||
|
||||
import provider
|
||||
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'provider.sphinx']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-oauth2-provider'
|
||||
copyright = u'2012, Alen Mujezinovic'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = provider.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = provider.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
#html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-oauth2-providerdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#latex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'django-oauth2-provider.tex', u'django-oauth2-provider Documentation',
|
||||
u'Alen Mujezinovic', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-oauth2-provider', u'django-oauth2-provider Documentation',
|
||||
[u'Alen Mujezinovic'], 1)
|
||||
]
|
|
@ -1,96 +0,0 @@
|
|||
Getting started
|
||||
===============
|
||||
|
||||
Installation
|
||||
############
|
||||
|
||||
.. sourcecode:: sh
|
||||
|
||||
$ pip install django-oauth2-provider
|
||||
|
||||
Configuration
|
||||
#############
|
||||
|
||||
Add OAuth2 Provider to :attr:`INSTALLED_APPS`
|
||||
---------------------------------------------
|
||||
|
||||
::
|
||||
|
||||
INSTALLED_APPS = (
|
||||
# ...
|
||||
'provider',
|
||||
'provider.oauth2',
|
||||
)
|
||||
|
||||
Modify your settings to match your needs
|
||||
----------------------------------------
|
||||
|
||||
The default settings are available in :attr:`provider.constants`.
|
||||
|
||||
|
||||
Include the OAuth 2 views
|
||||
-------------------------
|
||||
|
||||
Add :attr:`provider.oauth2.urls` to your root ``urls.py`` file.
|
||||
|
||||
::
|
||||
|
||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')),
|
||||
|
||||
|
||||
.. note:: The namespace argument is required.
|
||||
|
||||
Sync your database
|
||||
------------------
|
||||
|
||||
.. sourcecode:: sh
|
||||
|
||||
$ python manage.py syncdb
|
||||
$ python manage.py migrate
|
||||
|
||||
How to request an :attr:`access token` for the first time ?
|
||||
###########################################################
|
||||
|
||||
Create a :attr:`client` entry in your database
|
||||
----------------------------------------------
|
||||
|
||||
.. note:: To find out which type of :attr:`client` you need to create, read :rfc:`2.1`.
|
||||
|
||||
To create a new entry simply use the Django admin panel.
|
||||
|
||||
Request an access token
|
||||
-----------------------
|
||||
|
||||
Assuming that you've used the same URL configuration as above, your
|
||||
client needs to submit a :attr:`POST` request to
|
||||
:attr:`/oauth2/access_token` including the following parameters:
|
||||
|
||||
* ``client_id`` - The client ID you've configured in the Django admin.
|
||||
* ``client_secret`` - The client secret configured in the Django admin.
|
||||
* ``username`` - The username with which you want to log in.
|
||||
* ``password`` - The password corresponding to the user you're logging
|
||||
in with.
|
||||
|
||||
|
||||
**Request**
|
||||
|
||||
.. sourcecode:: sh
|
||||
|
||||
$ curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD" http://localhost:8000/oauth2/access_token/
|
||||
|
||||
**Response**
|
||||
|
||||
.. sourcecode:: json
|
||||
|
||||
{"access_token": "<your-access-token>", "scope": "read", "expires_in": 86399, "refresh_token": "<your-refresh-token>"}
|
||||
|
||||
|
||||
This particular way of obtaining an access token is called a **Password
|
||||
Grant**. All the other ways of acquiring an access token are outlined
|
||||
in :rfc:`4`.
|
||||
|
||||
.. note:: Remember that you should always use HTTPS for all your OAuth
|
||||
2 requests otherwise you won't be secured.
|
||||
|
||||
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
Welcome to django-oauth2-provider's documentation!
|
||||
==================================================
|
||||
|
||||
*django-oauth2-provider* is a Django application that provides customizable OAuth2_ authentication for your Django projects.
|
||||
|
||||
The default implementation makes reasonable assumptions about the allowed grant types and provides clients with two easy accessible URL endpoints. (:attr:`provider.oauth2.urls`)
|
||||
|
||||
If you require custom database backends, URLs, wish to extend the OAuth2_ protocol as defined in :rfc:`8` or anything else, you can override the default behaviours by subclassing the views in :attr:`provider.views` and add your specific use cases.
|
||||
|
||||
Getting started
|
||||
###############
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
getting_started
|
||||
|
||||
API
|
||||
###
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
api
|
||||
|
||||
Changes
|
||||
#######
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
changes
|
||||
|
||||
|
||||
Made by `Caffeinehit <http://www.caffeinehit.com/>`_.
|
||||
|
||||
.. _OAuth2: http://tools.ietf.org/html/rfc6749
|
10
manage.py
10
manage.py
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
|
@ -1,2 +0,0 @@
|
|||
__version__ = "0.2.7-dev"
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
user_model_label = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
|
||||
try:
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
except ImportError:
|
||||
def skipIfCustomUser(wrapped):
|
||||
return wrapped
|
|
@ -1,4 +0,0 @@
|
|||
try:
|
||||
from django.conf.urls import patterns, url, include
|
||||
except ImportError: # django 1.3
|
||||
from django.conf.urls.defaults import patterns, url, include
|
|
@ -1,43 +0,0 @@
|
|||
from datetime import timedelta
|
||||
from django.conf import settings
|
||||
|
||||
CONFIDENTIAL = 0
|
||||
PUBLIC = 1
|
||||
|
||||
CLIENT_TYPES = (
|
||||
(CONFIDENTIAL, "Confidential (Web applications)"),
|
||||
(PUBLIC, "Public (Native and JS applications)")
|
||||
)
|
||||
|
||||
RESPONSE_TYPE_CHOICES = getattr(settings, 'OAUTH_RESPONSE_TYPE_CHOICES', ("code", "token"))
|
||||
|
||||
TOKEN_TYPE = 'Bearer'
|
||||
|
||||
READ = 1 << 1
|
||||
WRITE = 1 << 2
|
||||
READ_WRITE = READ | WRITE
|
||||
|
||||
DEFAULT_SCOPES = (
|
||||
(READ, 'read'),
|
||||
(WRITE, 'write'),
|
||||
(READ_WRITE, 'read+write'),
|
||||
)
|
||||
|
||||
SCOPES = getattr(settings, 'OAUTH_SCOPES', DEFAULT_SCOPES)
|
||||
|
||||
EXPIRE_DELTA = getattr(settings, 'OAUTH_EXPIRE_DELTA', timedelta(days=365))
|
||||
|
||||
# Expiry delta for public clients (which typically have shorter lived tokens)
|
||||
EXPIRE_DELTA_PUBLIC = getattr(settings, 'OAUTH_EXPIRE_DELTA_PUBLIC', timedelta(days=30))
|
||||
|
||||
EXPIRE_CODE_DELTA = getattr(settings, 'OAUTH_EXPIRE_CODE_DELTA', timedelta(seconds=10 * 60))
|
||||
|
||||
# Remove expired tokens immediately instead of letting them persist.
|
||||
DELETE_EXPIRED = getattr(settings, 'OAUTH_DELETE_EXPIRED', False)
|
||||
|
||||
ENFORCE_SECURE = getattr(settings, 'OAUTH_ENFORCE_SECURE', False)
|
||||
ENFORCE_CLIENT_SECURE = getattr(settings, 'OAUTH_ENFORCE_CLIENT_SECURE', True)
|
||||
|
||||
SESSION_KEY = getattr(settings, 'OAUTH_SESSION_KEY', 'oauth')
|
||||
|
||||
SINGLE_ACCESS_TOKEN = getattr(settings, 'OAUTH_SINGLE_ACCESS_TOKEN', False)
|
|
@ -1,64 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class OAuthValidationError(Exception):
|
||||
"""
|
||||
Exception to throw inside :class:`OAuthForm` if any OAuth2 related errors
|
||||
are encountered such as invalid grant type, invalid client, etc.
|
||||
|
||||
:attr:`OAuthValidationError` expects a dictionary outlining the OAuth error
|
||||
as its first argument when instantiating.
|
||||
|
||||
:example:
|
||||
|
||||
::
|
||||
|
||||
class GrantValidationForm(OAuthForm):
|
||||
grant_type = forms.CharField()
|
||||
|
||||
def clean_grant(self):
|
||||
if not self.cleaned_data.get('grant_type') == 'code':
|
||||
raise OAuthValidationError({
|
||||
'error': 'invalid_grant',
|
||||
'error_description': "%s is not a valid grant type" % (
|
||||
self.cleaned_data.get('grant_type'))
|
||||
})
|
||||
|
||||
The different types of errors are outlined in :rfc:`4.2.2.1` and
|
||||
:rfc:`5.2`.
|
||||
"""
|
||||
|
||||
|
||||
class OAuthForm(forms.Form):
|
||||
"""
|
||||
Form class that creates shallow error dicts and exists early when a
|
||||
:class:`OAuthValidationError` is raised.
|
||||
|
||||
The shallow error dict is reused when returning error responses to the
|
||||
client.
|
||||
|
||||
The different types of errors are outlined in :rfc:`4.2.2.1` and
|
||||
:rfc:`5.2`.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.client = kwargs.pop('client', None)
|
||||
super(OAuthForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def _clean_fields(self):
|
||||
"""
|
||||
Overriding the default cleaning behaviour to exit early on errors
|
||||
instead of validating each field.
|
||||
"""
|
||||
try:
|
||||
super(OAuthForm, self)._clean_fields()
|
||||
except OAuthValidationError, e:
|
||||
self._errors.update(e.args[0])
|
||||
|
||||
def _clean_form(self):
|
||||
"""
|
||||
Overriding the default cleaning behaviour for a shallow error dict.
|
||||
"""
|
||||
try:
|
||||
super(OAuthForm, self)._clean_form()
|
||||
except OAuthValidationError, e:
|
||||
self._errors.update(e.args[0])
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,8 +0,0 @@
|
|||
import backends
|
||||
import forms
|
||||
import managers
|
||||
import models
|
||||
import urls
|
||||
import views
|
||||
|
||||
default_app_config = 'provider.oauth2.apps.Oauth2'
|
|
@ -1,22 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import AccessToken, Grant, Client, RefreshToken
|
||||
|
||||
|
||||
class AccessTokenAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'client', 'token', 'expires', 'scope',)
|
||||
raw_id_fields = ('user',)
|
||||
|
||||
|
||||
class GrantAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'client', 'code', 'expires',)
|
||||
raw_id_fields = ('user',)
|
||||
|
||||
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
list_display = ('url', 'user', 'redirect_uri', 'client_id', 'client_type')
|
||||
raw_id_fields = ('user',)
|
||||
|
||||
admin.site.register(AccessToken, AccessTokenAdmin)
|
||||
admin.site.register(Grant, GrantAdmin)
|
||||
admin.site.register(Client, ClientAdmin)
|
||||
admin.site.register(RefreshToken)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class Oauth2(AppConfig):
|
||||
name = 'provider.oauth2'
|
||||
label = 'oauth2'
|
||||
verbose_name = "Provider Oauth2"
|
|
@ -1,95 +0,0 @@
|
|||
from ..utils import now
|
||||
from .forms import ClientAuthForm, PublicPasswordGrantForm
|
||||
from .models import AccessToken
|
||||
|
||||
|
||||
class BaseBackend(object):
|
||||
"""
|
||||
Base backend used to authenticate clients as defined in :rfc:`1` against
|
||||
our database.
|
||||
"""
|
||||
def authenticate(self, request=None):
|
||||
"""
|
||||
Override this method to implement your own authentication backend.
|
||||
Return a client or ``None`` in case of failure.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BasicClientBackend(object):
|
||||
"""
|
||||
Backend that tries to authenticate a client through HTTP authorization
|
||||
headers as defined in :rfc:`2.3.1`.
|
||||
"""
|
||||
def authenticate(self, request=None):
|
||||
auth = request.META.get('HTTP_AUTHORIZATION')
|
||||
|
||||
if auth is None or auth == '':
|
||||
return None
|
||||
|
||||
try:
|
||||
basic, base64 = auth.split(' ')
|
||||
client_id, client_secret = base64.decode('base64').split(':')
|
||||
|
||||
form = ClientAuthForm({
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret})
|
||||
|
||||
if form.is_valid():
|
||||
return form.cleaned_data.get('client')
|
||||
return None
|
||||
|
||||
except ValueError:
|
||||
# Auth header was malformed, unpacking went wrong
|
||||
return None
|
||||
|
||||
|
||||
class RequestParamsClientBackend(object):
|
||||
"""
|
||||
Backend that tries to authenticate a client through request parameters
|
||||
which might be in the request body or URI as defined in :rfc:`2.3.1`.
|
||||
"""
|
||||
def authenticate(self, request=None):
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
form = ClientAuthForm(request.REQUEST)
|
||||
|
||||
if form.is_valid():
|
||||
return form.cleaned_data.get('client')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PublicPasswordBackend(object):
|
||||
"""
|
||||
Backend that tries to authenticate a client using username, password
|
||||
and client ID. This is only available in specific circumstances:
|
||||
|
||||
- grant_type is "password"
|
||||
- client.client_type is 'public'
|
||||
"""
|
||||
|
||||
def authenticate(self, request=None):
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
form = PublicPasswordGrantForm(request.REQUEST)
|
||||
|
||||
if form.is_valid():
|
||||
return form.cleaned_data.get('client')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class AccessTokenBackend(object):
|
||||
"""
|
||||
Authenticate a user via access token and client object.
|
||||
"""
|
||||
|
||||
def authenticate(self, access_token=None, client=None):
|
||||
try:
|
||||
return AccessToken.objects.get(token=access_token,
|
||||
expires__gt=now(), client=client)
|
||||
except AccessToken.DoesNotExist:
|
||||
return None
|
|
@ -1,62 +0,0 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"redirect_uri": "http://example.com/application/1/",
|
||||
"client_id": "90a4a24ffefe7ebbae2c",
|
||||
"client_secret": "35c25066023f32c4f098d1e40de94f07f98c1acf",
|
||||
"client_type": 0,
|
||||
"url": "http://example.com/",
|
||||
"user": 1
|
||||
},
|
||||
"model": "oauth2.client",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"redirect_uri": "http://example.com/application/2/",
|
||||
"client_id": "71fbc29950ac1b386a12",
|
||||
"client_secret": "1944b695ca0cbf4f419a7d5c7e4fed13a660bc04",
|
||||
"client_type": 0,
|
||||
"url": "http://example.com/",
|
||||
"user": 2
|
||||
},
|
||||
"model": "oauth2.client",
|
||||
"pk": 2
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"date_joined": "2012-01-23 05:44:17",
|
||||
"email": "test-1@example.com",
|
||||
"first_name": "",
|
||||
"groups": [],
|
||||
"is_active": true,
|
||||
"is_staff": true,
|
||||
"is_superuser": true,
|
||||
"last_login": "2012-01-23 05:52:32",
|
||||
"last_name": "",
|
||||
"password": "sha1$da29e$498b9faab2d002183bc1d874689634b0e15ad6d7",
|
||||
"user_permissions": [],
|
||||
"username": "test-user-1"
|
||||
},
|
||||
"model": "auth.user",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"date_joined": "2012-01-23 05:53:31",
|
||||
"email": "",
|
||||
"first_name": "",
|
||||
"groups": [],
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2012-01-23 05:53:31",
|
||||
"last_name": "",
|
||||
"password": "sha1$0cf1b$d66589690edd96b410170fcae5cc2bdfb68821e7",
|
||||
"user_permissions": [],
|
||||
"username": "test-user-2"
|
||||
},
|
||||
"model": "auth.user",
|
||||
"pk": 2
|
||||
}
|
||||
]
|
|
@ -1,335 +0,0 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import authenticate
|
||||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.translation import ugettext as _
|
||||
from .. import scope
|
||||
from ..constants import RESPONSE_TYPE_CHOICES, SCOPES
|
||||
from ..forms import OAuthForm, OAuthValidationError
|
||||
from ..scope import SCOPE_NAMES
|
||||
from ..utils import now
|
||||
from .models import Client, Grant, RefreshToken
|
||||
|
||||
|
||||
class ClientForm(forms.ModelForm):
|
||||
"""
|
||||
Form to create new consumers.
|
||||
"""
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = ('name', 'url', 'redirect_uri', 'client_type')
|
||||
|
||||
def save(self, user=None, **kwargs):
|
||||
self.instance.user = user
|
||||
return super(ClientForm, self).save(**kwargs)
|
||||
|
||||
|
||||
class ClientAuthForm(forms.Form):
|
||||
"""
|
||||
Client authentication form. Required to make sure that we're dealing with a
|
||||
real client. Form is used in :attr:`provider.oauth2.backends` to validate
|
||||
the client.
|
||||
"""
|
||||
client_id = forms.CharField()
|
||||
client_secret = forms.CharField()
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
try:
|
||||
client = Client.objects.get(client_id=data.get('client_id'),
|
||||
client_secret=data.get('client_secret'))
|
||||
except Client.DoesNotExist:
|
||||
raise forms.ValidationError(_("Client could not be validated with "
|
||||
"key pair."))
|
||||
|
||||
data['client'] = client
|
||||
return data
|
||||
|
||||
|
||||
class ScopeChoiceField(forms.ChoiceField):
|
||||
"""
|
||||
Custom form field that seperates values on space as defined in
|
||||
:rfc:`3.3`.
|
||||
"""
|
||||
widget = forms.SelectMultiple
|
||||
|
||||
def to_python(self, value):
|
||||
if not value:
|
||||
return []
|
||||
|
||||
# New in Django 1.6: value may come in as a string.
|
||||
# Instead of raising an `OAuthValidationError`, try to parse and
|
||||
# ultimately return an empty list if nothing remains -- this will
|
||||
# eventually raise an `OAuthValidationError` in `validate` where
|
||||
# it should be anyways.
|
||||
if not isinstance(value, (list, tuple)):
|
||||
value = value.split(' ')
|
||||
|
||||
# Split values into list
|
||||
return u' '.join([smart_unicode(val) for val in value]).split(u' ')
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Validates that the input is a list or tuple.
|
||||
"""
|
||||
if self.required and not value:
|
||||
raise OAuthValidationError({'error': 'invalid_request'})
|
||||
|
||||
# Validate that each value in the value list is in self.choices.
|
||||
for val in value:
|
||||
if not self.valid_value(val):
|
||||
raise OAuthValidationError({
|
||||
'error': 'invalid_request',
|
||||
'error_description': _("'%s' is not a valid scope.") % \
|
||||
val})
|
||||
|
||||
|
||||
class ScopeMixin(object):
|
||||
"""
|
||||
Form mixin to clean scope fields.
|
||||
"""
|
||||
def clean_scope(self):
|
||||
"""
|
||||
The scope is assembled by combining all the set flags into a single
|
||||
integer value which we can later check again for set bits.
|
||||
|
||||
If *no* scope is set, we return the default scope which is the first
|
||||
defined scope in :attr:`provider.constants.SCOPES`.
|
||||
|
||||
"""
|
||||
default = SCOPES[0][0]
|
||||
|
||||
flags = self.cleaned_data.get('scope', [])
|
||||
|
||||
return scope.to_int(default=default, *flags)
|
||||
|
||||
|
||||
class AuthorizationRequestForm(ScopeMixin, OAuthForm):
|
||||
"""
|
||||
This form is used to validate the request data that the authorization
|
||||
endpoint receives from clients.
|
||||
|
||||
Included data is specified in :rfc:`4.1.1`.
|
||||
"""
|
||||
# Setting all required fields to false to explicitly check by hand
|
||||
# and use custom error messages that can be reused in the OAuth2
|
||||
# protocol
|
||||
response_type = forms.CharField(required=False)
|
||||
"""
|
||||
``"code"`` or ``"token"`` depending on the grant type.
|
||||
"""
|
||||
|
||||
redirect_uri = forms.URLField(required=False)
|
||||
"""
|
||||
Where the client would like to redirect the user
|
||||
back to. This has to match whatever value was saved while creating
|
||||
the client.
|
||||
"""
|
||||
|
||||
state = forms.CharField(required=False)
|
||||
"""
|
||||
Opaque - just pass back to the client for validation.
|
||||
"""
|
||||
|
||||
scope = ScopeChoiceField(choices=SCOPE_NAMES, required=False)
|
||||
"""
|
||||
The scope that the authorization should include.
|
||||
"""
|
||||
|
||||
def clean_response_type(self):
|
||||
"""
|
||||
:rfc:`3.1.1` Lists of values are space delimited.
|
||||
"""
|
||||
response_type = self.cleaned_data.get('response_type')
|
||||
|
||||
if not response_type:
|
||||
raise OAuthValidationError({'error': 'invalid_request',
|
||||
'error_description': "No 'response_type' supplied."})
|
||||
|
||||
types = response_type.split(" ")
|
||||
|
||||
for type in types:
|
||||
if type not in RESPONSE_TYPE_CHOICES:
|
||||
raise OAuthValidationError({
|
||||
'error': 'unsupported_response_type',
|
||||
'error_description': u"'%s' is not a supported response "
|
||||
"type." % type})
|
||||
|
||||
return response_type
|
||||
|
||||
def clean_redirect_uri(self):
|
||||
"""
|
||||
:rfc:`3.1.2` The redirect value has to match what was saved on the
|
||||
authorization server.
|
||||
"""
|
||||
redirect_uri = self.cleaned_data.get('redirect_uri')
|
||||
|
||||
if redirect_uri:
|
||||
if not redirect_uri == self.client.redirect_uri:
|
||||
raise OAuthValidationError({
|
||||
'error': 'invalid_request',
|
||||
'error_description': _("The requested redirect didn't "
|
||||
"match the client settings.")})
|
||||
|
||||
return redirect_uri
|
||||
|
||||
|
||||
class AuthorizationForm(ScopeMixin, OAuthForm):
|
||||
"""
|
||||
A form used to ask the resource owner for authorization of a given client.
|
||||
"""
|
||||
authorize = forms.BooleanField(required=False)
|
||||
scope = ScopeChoiceField(choices=SCOPE_NAMES, required=False)
|
||||
|
||||
def save(self, **kwargs):
|
||||
authorize = self.cleaned_data.get('authorize')
|
||||
|
||||
if not authorize:
|
||||
return None
|
||||
|
||||
grant = Grant()
|
||||
grant.scope = self.cleaned_data.get('scope')
|
||||
return grant
|
||||
|
||||
|
||||
class RefreshTokenGrantForm(ScopeMixin, OAuthForm):
|
||||
"""
|
||||
Checks and returns a refresh token.
|
||||
"""
|
||||
refresh_token = forms.CharField(required=False)
|
||||
scope = ScopeChoiceField(choices=SCOPE_NAMES, required=False)
|
||||
|
||||
def clean_refresh_token(self):
|
||||
token = self.cleaned_data.get('refresh_token')
|
||||
|
||||
if not token:
|
||||
raise OAuthValidationError({'error': 'invalid_request'})
|
||||
|
||||
try:
|
||||
token = RefreshToken.objects.get(token=token,
|
||||
expired=False, client=self.client)
|
||||
except RefreshToken.DoesNotExist:
|
||||
raise OAuthValidationError({'error': 'invalid_grant'})
|
||||
|
||||
return token
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Make sure that the scope is less or equal to the previous scope!
|
||||
"""
|
||||
data = self.cleaned_data
|
||||
want_scope = data.get('scope') or 0
|
||||
refresh_token = data.get('refresh_token')
|
||||
access_token = getattr(refresh_token, 'access_token', None) if \
|
||||
refresh_token else \
|
||||
None
|
||||
has_scope = access_token.scope if access_token else 0
|
||||
|
||||
# Only check if we've actually got a scope in the data
|
||||
# (read: All fields have been cleaned)
|
||||
if want_scope is not 0 and not scope.check(want_scope, has_scope):
|
||||
raise OAuthValidationError({'error': 'invalid_scope'})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AuthorizationCodeGrantForm(ScopeMixin, OAuthForm):
|
||||
"""
|
||||
Check and return an authorization grant.
|
||||
"""
|
||||
code = forms.CharField(required=False)
|
||||
scope = ScopeChoiceField(choices=SCOPE_NAMES, required=False)
|
||||
|
||||
def clean_code(self):
|
||||
code = self.cleaned_data.get('code')
|
||||
|
||||
if not code:
|
||||
raise OAuthValidationError({'error': 'invalid_request'})
|
||||
|
||||
try:
|
||||
self.cleaned_data['grant'] = Grant.objects.get(
|
||||
code=code, client=self.client, expires__gt=now())
|
||||
except Grant.DoesNotExist:
|
||||
raise OAuthValidationError({'error': 'invalid_grant'})
|
||||
|
||||
return code
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Make sure that the scope is less or equal to the scope allowed on the
|
||||
grant!
|
||||
"""
|
||||
data = self.cleaned_data
|
||||
want_scope = data.get('scope') or 0
|
||||
grant = data.get('grant')
|
||||
has_scope = grant.scope if grant else 0
|
||||
|
||||
# Only check if we've actually got a scope in the data
|
||||
# (read: All fields have been cleaned)
|
||||
if want_scope is not 0 and not scope.check(want_scope, has_scope):
|
||||
raise OAuthValidationError({'error': 'invalid_scope'})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class PasswordGrantForm(ScopeMixin, OAuthForm):
|
||||
"""
|
||||
Validate the password of a user on a password grant request.
|
||||
"""
|
||||
username = forms.CharField(required=False)
|
||||
password = forms.CharField(required=False)
|
||||
scope = ScopeChoiceField(choices=SCOPE_NAMES, required=False)
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
|
||||
if not username:
|
||||
raise OAuthValidationError({'error': 'invalid_request'})
|
||||
|
||||
return username
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if not password:
|
||||
raise OAuthValidationError({'error': 'invalid_request'})
|
||||
|
||||
return password
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
|
||||
user = authenticate(username=data.get('username'),
|
||||
password=data.get('password'))
|
||||
|
||||
if user is None:
|
||||
raise OAuthValidationError({'error': 'invalid_grant'})
|
||||
|
||||
data['user'] = user
|
||||
return data
|
||||
|
||||
|
||||
class PublicPasswordGrantForm(PasswordGrantForm):
|
||||
client_id = forms.CharField(required=True)
|
||||
grant_type = forms.CharField(required=True)
|
||||
|
||||
def clean_grant_type(self):
|
||||
grant_type = self.cleaned_data.get('grant_type')
|
||||
|
||||
if grant_type != 'password':
|
||||
raise OAuthValidationError({'error': 'invalid_grant'})
|
||||
|
||||
return grant_type
|
||||
|
||||
def clean(self):
|
||||
data = super(PublicPasswordGrantForm, self).clean()
|
||||
|
||||
try:
|
||||
client = Client.objects.get(client_id=data.get('client_id'))
|
||||
except Client.DoesNotExist:
|
||||
raise OAuthValidationError({'error': 'invalid_client'})
|
||||
|
||||
if client.client_type != 1: # public
|
||||
raise OAuthValidationError({'error': 'invalid_client'})
|
||||
|
||||
data['client'] = client
|
||||
return data
|
|
@ -1,7 +0,0 @@
|
|||
from ..utils import now
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AccessTokenManager(models.Manager):
|
||||
def get_token(self, token):
|
||||
return self.get(token=token, expires__gt=now())
|
|
@ -1,153 +0,0 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
from provider.compat import user_model_label
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding model 'Client'
|
||||
db.create_table('oauth2_client', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_model_label])),
|
||||
('url', self.gf('django.db.models.fields.URLField')(max_length=200)),
|
||||
('redirect_uri', self.gf('django.db.models.fields.URLField')(max_length=200)),
|
||||
('client_id', self.gf('django.db.models.fields.CharField')(default='37b581bdc702c732aa65', max_length=255)),
|
||||
('client_secret', self.gf('django.db.models.fields.CharField')(default='5cf90561f7566aa81457f8a32187dcb8147c7b73', max_length=255)),
|
||||
('client_type', self.gf('django.db.models.fields.IntegerField')()),
|
||||
))
|
||||
db.send_create_signal('oauth2', ['Client'])
|
||||
|
||||
# Adding model 'Grant'
|
||||
db.create_table('oauth2_grant', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_model_label])),
|
||||
('client', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['oauth2.Client'])),
|
||||
('code', self.gf('django.db.models.fields.CharField')(default='f0cda1a5f4ae915431ff93f477c012b38e2429c4', max_length=255)),
|
||||
('expires', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 2, 8, 10, 43, 45, 620301))),
|
||||
('redirect_uri', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
|
||||
('scope', self.gf('django.db.models.fields.IntegerField')(default=0)),
|
||||
))
|
||||
db.send_create_signal('oauth2', ['Grant'])
|
||||
|
||||
# Adding model 'AccessToken'
|
||||
db.create_table('oauth2_accesstoken', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_model_label])),
|
||||
('token', self.gf('django.db.models.fields.CharField')(default='b10b8f721e95117cb13c', max_length=255)),
|
||||
('client', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['oauth2.Client'])),
|
||||
('expires', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2013, 2, 7, 10, 33, 45, 618854))),
|
||||
('scope', self.gf('django.db.models.fields.IntegerField')(default=0)),
|
||||
))
|
||||
db.send_create_signal('oauth2', ['AccessToken'])
|
||||
|
||||
# Adding model 'RefreshToken'
|
||||
db.create_table('oauth2_refreshtoken', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_model_label])),
|
||||
('token', self.gf('django.db.models.fields.CharField')(default='84035a870dab7c820c2c501fb0b10f86fdf7a3fe', max_length=255)),
|
||||
('access_token', self.gf('django.db.models.fields.related.OneToOneField')(related_name='refresh_token', unique=True, to=orm['oauth2.AccessToken'])),
|
||||
('client', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['oauth2.Client'])),
|
||||
('expired', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
))
|
||||
db.send_create_signal('oauth2', ['RefreshToken'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting model 'Client'
|
||||
db.delete_table('oauth2_client')
|
||||
|
||||
# Deleting model 'Grant'
|
||||
db.delete_table('oauth2_grant')
|
||||
|
||||
# Deleting model 'AccessToken'
|
||||
db.delete_table('oauth2_accesstoken')
|
||||
|
||||
# Deleting model 'RefreshToken'
|
||||
db.delete_table('oauth2_refreshtoken')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
user_model_label: {
|
||||
'Meta': {'object_name': user_model_label.split('.')[-1]},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'oauth2.accesstoken': {
|
||||
'Meta': {'object_name': 'AccessToken'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 7, 10, 33, 45, 624553)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'d5c1f65020ebdc89f20c'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.client': {
|
||||
'Meta': {'object_name': 'Client'},
|
||||
'client_id': ('django.db.models.fields.CharField', [], {'default': "'306fb26cbcc87dd33cdb'", 'max_length': '255'}),
|
||||
'client_secret': ('django.db.models.fields.CharField', [], {'default': "'7e5785add4898448d53767f15373636b918cf0e3'", 'max_length': '255'}),
|
||||
'client_type': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.grant': {
|
||||
'Meta': {'object_name': 'Grant'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'code': ('django.db.models.fields.CharField', [], {'default': "'310b2c63e27306ecf5307569dd62340cc4994b73'", 'max_length': '255'}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 2, 8, 10, 43, 45, 625956)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.refreshtoken': {
|
||||
'Meta': {'object_name': 'RefreshToken'},
|
||||
'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': "orm['oauth2.AccessToken']"}),
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'ef0ab76037f17769ab2975a816e8f41a1c11d25e'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['oauth2']
|
|
@ -1,101 +0,0 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
from provider.compat import user_model_label
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'Client.user'
|
||||
db.alter_column('oauth2_client', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_model_label], null=True))
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# User chose to not deal with backwards NULL issues for 'Client.user'
|
||||
raise RuntimeError("Cannot reverse this migration. 'Client.user' and its values cannot be restored.")
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
user_model_label: {
|
||||
'Meta': {'object_name': user_model_label.split('.')[-1]},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'oauth2.accesstoken': {
|
||||
'Meta': {'object_name': 'AccessToken'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 7, 10, 34, 7, 491068)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'ed2ee3f5209076916309'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.client': {
|
||||
'Meta': {'object_name': 'Client'},
|
||||
'client_id': ('django.db.models.fields.CharField', [], {'default': "'b1f489e6693d6ba20770'", 'max_length': '255'}),
|
||||
'client_secret': ('django.db.models.fields.CharField', [], {'default': "'ac43f96040354c910c8e57a4952d9a8e92f83555'", 'max_length': '255'}),
|
||||
'client_type': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label, 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'oauth2.grant': {
|
||||
'Meta': {'object_name': 'Grant'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'code': ('django.db.models.fields.CharField', [], {'default': "'d62c4606c1d7743b59480a85bf8a691aec67df0a'", 'max_length': '255'}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 2, 8, 10, 44, 7, 492506)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.refreshtoken': {
|
||||
'Meta': {'object_name': 'RefreshToken'},
|
||||
'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': "orm['oauth2.AccessToken']"}),
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'9b79a016e6b5220883d0d576f34bcbee29ce36b1'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['oauth2']
|
|
@ -1,102 +0,0 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
from provider.compat import user_model_label
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding field 'Client.name'
|
||||
db.add_column('oauth2_client', 'name', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting field 'Client.name'
|
||||
db.delete_column('oauth2_client', 'name')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
user_model_label: {
|
||||
'Meta': {'object_name': user_model_label.split('.')[-1]},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'oauth2.accesstoken': {
|
||||
'Meta': {'object_name': 'AccessToken'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 7, 10, 40, 0, 790902)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'d9373f4d09149181e1c5'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.client': {
|
||||
'Meta': {'object_name': 'Client'},
|
||||
'client_id': ('django.db.models.fields.CharField', [], {'default': "'00385d74e0b239deac42'", 'max_length': '255'}),
|
||||
'client_secret': ('django.db.models.fields.CharField', [], {'default': "'460de077126ccc746473f8b2afab6a8aef9f542a'", 'max_length': '255'}),
|
||||
'client_type': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label, 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'oauth2.grant': {
|
||||
'Meta': {'object_name': 'Grant'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'code': ('django.db.models.fields.CharField', [], {'default': "'ae889a1d0fc0c8569b47a7037c3d2da5cb7d0d43'", 'max_length': '255'}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 2, 8, 10, 50, 0, 788579)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
'oauth2.refreshtoken': {
|
||||
'Meta': {'object_name': 'RefreshToken'},
|
||||
'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': "orm['oauth2.AccessToken']"}),
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'50d5f3e805e02073364c4ebfc55a94328afe25bd'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_label})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['oauth2']
|
|
@ -1,98 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding index on 'AccessToken', fields ['token']
|
||||
db.create_index('oauth2_accesstoken', ['token'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'AccessToken', fields ['token']
|
||||
db.delete_index('oauth2_accesstoken', ['token'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'oauth2.accesstoken': {
|
||||
'Meta': {'object_name': 'AccessToken'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 8, 8, 0, 0)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'ab8c8bcd91e8750462b631516b60b0b95dffe1f4'", 'max_length': '255', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'oauth2.client': {
|
||||
'Meta': {'object_name': 'Client'},
|
||||
'client_id': ('django.db.models.fields.CharField', [], {'default': "'0a8e54e38c024606ba0a'", 'max_length': '255'}),
|
||||
'client_secret': ('django.db.models.fields.CharField', [], {'default': "'e53ddb9736f9eea65100885a1b20fb5f2bb0fb4d'", 'max_length': '255'}),
|
||||
'client_type': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'oauth2_client'", 'null': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'oauth2.grant': {
|
||||
'Meta': {'object_name': 'Grant'},
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'code': ('django.db.models.fields.CharField', [], {'default': "'5e0ca84e98678a3b55b8901e85a20f995672aea2'", 'max_length': '255'}),
|
||||
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 8, 8, 0, 0)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'oauth2.refreshtoken': {
|
||||
'Meta': {'object_name': 'RefreshToken'},
|
||||
'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': "orm['oauth2.AccessToken']"}),
|
||||
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
|
||||
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'token': ('django.db.models.fields.CharField', [], {'default': "'32e9eb7edda764ba8f752ae49223d42acea7cb88'", 'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['oauth2']
|
|
@ -1,206 +0,0 @@
|
|||
"""
|
||||
Default model implementations. Custom database or OAuth backends need to
|
||||
implement these models with fields and and methods to be compatible with the
|
||||
views in :attr:`provider.views`.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from .. import constants
|
||||
from ..constants import CLIENT_TYPES
|
||||
from ..utils import now, short_token, long_token, get_code_expiry
|
||||
from ..utils import get_token_expiry, serialize_instance, deserialize_instance
|
||||
from .managers import AccessTokenManager
|
||||
|
||||
try:
|
||||
from django.utils import timezone
|
||||
except ImportError:
|
||||
timezone = None
|
||||
|
||||
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
"""
|
||||
Default client implementation.
|
||||
|
||||
Expected fields:
|
||||
|
||||
* :attr:`user`
|
||||
* :attr:`name`
|
||||
* :attr:`url`
|
||||
* :attr:`redirect_url`
|
||||
* :attr:`client_id`
|
||||
* :attr:`client_secret`
|
||||
* :attr:`client_type`
|
||||
|
||||
Clients are outlined in the :rfc:`2` and its subsections.
|
||||
"""
|
||||
user = models.ForeignKey(AUTH_USER_MODEL, related_name='oauth2_client',
|
||||
blank=True, null=True)
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
url = models.URLField(help_text="Your application's URL.")
|
||||
redirect_uri = models.URLField(help_text="Your application's callback URL")
|
||||
client_id = models.CharField(max_length=255, default=short_token)
|
||||
client_secret = models.CharField(max_length=255, default=long_token)
|
||||
client_type = models.IntegerField(choices=CLIENT_TYPES)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.redirect_uri
|
||||
|
||||
def get_default_token_expiry(self):
|
||||
public = (self.client_type == 1)
|
||||
return get_token_expiry(public)
|
||||
|
||||
def serialize(self):
|
||||
return dict(user=serialize_instance(self.user),
|
||||
name=self.name,
|
||||
url=self.url,
|
||||
redirect_uri=self.redirect_uri,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
client_type=self.client_type)
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
kwargs = {}
|
||||
|
||||
# extract values that we care about
|
||||
for field in cls._meta.fields:
|
||||
name = field.name
|
||||
val = data.get(field.name, None)
|
||||
|
||||
# handle relations
|
||||
if val and field.rel:
|
||||
val = deserialize_instance(field.rel.to, val)
|
||||
|
||||
kwargs[name] = val
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
class Meta:
|
||||
app_label = 'oauth2'
|
||||
db_table = 'oauth2_client'
|
||||
|
||||
|
||||
class Grant(models.Model):
|
||||
"""
|
||||
Default grant implementation. A grant is a code that can be swapped for an
|
||||
access token. Grants have a limited lifetime as defined by
|
||||
:attr:`provider.constants.EXPIRE_CODE_DELTA` and outlined in
|
||||
:rfc:`4.1.2`
|
||||
|
||||
Expected fields:
|
||||
|
||||
* :attr:`user`
|
||||
* :attr:`client` - :class:`Client`
|
||||
* :attr:`code`
|
||||
* :attr:`expires` - :attr:`datetime.datetime`
|
||||
* :attr:`redirect_uri`
|
||||
* :attr:`scope`
|
||||
"""
|
||||
user = models.ForeignKey(AUTH_USER_MODEL)
|
||||
client = models.ForeignKey(Client)
|
||||
code = models.CharField(max_length=255, default=long_token)
|
||||
expires = models.DateTimeField(default=get_code_expiry)
|
||||
redirect_uri = models.CharField(max_length=255, blank=True)
|
||||
scope = models.IntegerField(default=0)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.code
|
||||
|
||||
class Meta:
|
||||
app_label = 'oauth2'
|
||||
db_table = 'oauth2_grant'
|
||||
|
||||
|
||||
class AccessToken(models.Model):
|
||||
"""
|
||||
Default access token implementation. An access token is a time limited
|
||||
token to access a user's resources.
|
||||
|
||||
Access tokens are outlined :rfc:`5`.
|
||||
|
||||
Expected fields:
|
||||
|
||||
* :attr:`user`
|
||||
* :attr:`token`
|
||||
* :attr:`client` - :class:`Client`
|
||||
* :attr:`expires` - :attr:`datetime.datetime`
|
||||
* :attr:`scope`
|
||||
|
||||
Expected methods:
|
||||
|
||||
* :meth:`get_expire_delta` - returns an integer representing seconds to
|
||||
expiry
|
||||
"""
|
||||
user = models.ForeignKey(AUTH_USER_MODEL)
|
||||
token = models.CharField(max_length=255, default=long_token, db_index=True)
|
||||
client = models.ForeignKey(Client)
|
||||
expires = models.DateTimeField()
|
||||
scope = models.IntegerField(default=constants.SCOPES[0][0],
|
||||
choices=constants.SCOPES)
|
||||
|
||||
objects = AccessTokenManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.token
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.expires:
|
||||
self.expires = self.client.get_default_token_expiry()
|
||||
super(AccessToken, self).save(*args, **kwargs)
|
||||
|
||||
def get_expire_delta(self, reference=None):
|
||||
"""
|
||||
Return the number of seconds until this token expires.
|
||||
"""
|
||||
if reference is None:
|
||||
reference = now()
|
||||
expiration = self.expires
|
||||
|
||||
if timezone:
|
||||
if timezone.is_aware(reference) and timezone.is_naive(expiration):
|
||||
# MySQL doesn't support timezone for datetime fields
|
||||
# so we assume that the date was stored in the UTC timezone
|
||||
expiration = timezone.make_aware(expiration, timezone.utc)
|
||||
elif timezone.is_naive(reference) and timezone.is_aware(expiration):
|
||||
reference = timezone.make_aware(reference, timezone.utc)
|
||||
|
||||
timedelta = expiration - reference
|
||||
return timedelta.days*86400 + timedelta.seconds
|
||||
|
||||
class Meta:
|
||||
app_label = 'oauth2'
|
||||
db_table = 'oauth2_accesstoken'
|
||||
|
||||
|
||||
class RefreshToken(models.Model):
|
||||
"""
|
||||
Default refresh token implementation. A refresh token can be swapped for a
|
||||
new access token when said token expires.
|
||||
|
||||
Expected fields:
|
||||
|
||||
* :attr:`user`
|
||||
* :attr:`token`
|
||||
* :attr:`access_token` - :class:`AccessToken`
|
||||
* :attr:`client` - :class:`Client`
|
||||
* :attr:`expired` - ``boolean``
|
||||
"""
|
||||
user = models.ForeignKey(AUTH_USER_MODEL)
|
||||
token = models.CharField(max_length=255, default=long_token)
|
||||
access_token = models.OneToOneField(AccessToken,
|
||||
related_name='refresh_token')
|
||||
client = models.ForeignKey(Client)
|
||||
expired = models.BooleanField(default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.token
|
||||
|
||||
class Meta:
|
||||
app_label = 'oauth2'
|
||||
db_table = 'oauth2_refreshtoken'
|
|
@ -1,623 +0,0 @@
|
|||
import json
|
||||
import urlparse
|
||||
import datetime
|
||||
from django.http import QueryDict
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.html import escape
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from .. import constants, scope
|
||||
from ..compat import skipIfCustomUser
|
||||
from ..templatetags.scope import scopes
|
||||
from ..utils import now as date_now
|
||||
from .forms import ClientForm
|
||||
from .models import Client, Grant, AccessToken, RefreshToken
|
||||
from .backends import BasicClientBackend, RequestParamsClientBackend
|
||||
from .backends import AccessTokenBackend
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class BaseOAuth2TestCase(TestCase):
|
||||
def login(self):
|
||||
self.client.login(username='test-user-1', password='test')
|
||||
|
||||
def auth_url(self):
|
||||
return reverse('oauth2:capture')
|
||||
|
||||
def auth_url2(self):
|
||||
return reverse('oauth2:authorize')
|
||||
|
||||
def redirect_url(self):
|
||||
return reverse('oauth2:redirect')
|
||||
|
||||
def access_token_url(self):
|
||||
return reverse('oauth2:access_token')
|
||||
|
||||
def get_client(self):
|
||||
return Client.objects.get(id=2)
|
||||
|
||||
def get_grant(self):
|
||||
return Grant.objects.all()[0]
|
||||
|
||||
def get_user(self):
|
||||
return User.objects.get(id=1)
|
||||
|
||||
def get_password(self):
|
||||
return 'test'
|
||||
|
||||
def _login_and_authorize(self, url_func=None):
|
||||
if url_func is None:
|
||||
url_func = lambda: self.auth_url() + '?client_id=%s&response_type=code&state=abc' % self.get_client().client_id
|
||||
|
||||
response = self.client.get(url_func())
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
response = self.client.post(self.auth_url2(), {'authorize': True, 'scope': constants.SCOPES[0][1]})
|
||||
self.assertEqual(302, response.status_code, response.content)
|
||||
self.assertTrue(self.redirect_url() in response['Location'])
|
||||
|
||||
|
||||
class AuthorizationTest(BaseOAuth2TestCase):
|
||||
fixtures = ['test_oauth2']
|
||||
|
||||
def setUp(self):
|
||||
self._old_login = settings.LOGIN_URL
|
||||
settings.LOGIN_URL = '/login/'
|
||||
|
||||
def tearDown(self):
|
||||
settings.LOGIN_URL = self._old_login
|
||||
|
||||
def test_authorization_requires_login(self):
|
||||
response = self.client.get(self.auth_url())
|
||||
|
||||
# Login redirect
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.assertEqual('/login/', urlparse.urlparse(response['Location']).path)
|
||||
|
||||
self.login()
|
||||
|
||||
response = self.client.get(self.auth_url())
|
||||
|
||||
self.assertEqual(302, response.status_code)
|
||||
|
||||
self.assertTrue(self.auth_url2() in response['Location'])
|
||||
|
||||
def test_authorization_requires_client_id(self):
|
||||
self.login()
|
||||
response = self.client.get(self.auth_url())
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue("An unauthorized client tried to access your resources." in response.content)
|
||||
|
||||
def test_authorization_rejects_invalid_client_id(self):
|
||||
self.login()
|
||||
response = self.client.get(self.auth_url() + '?client_id=123')
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue("An unauthorized client tried to access your resources." in response.content)
|
||||
|
||||
def test_authorization_requires_response_type(self):
|
||||
self.login()
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s' % self.get_client().client_id)
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue(escape(u"No 'response_type' supplied.") in response.content)
|
||||
|
||||
def test_authorization_requires_supported_response_type(self):
|
||||
self.login()
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=unsupported' % self.get_client().client_id)
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue(escape(u"'unsupported' is not a supported response type.") in response.content)
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code' % self.get_client().client_id)
|
||||
response = self.client.get(self.auth_url2())
|
||||
self.assertEqual(200, response.status_code, response.content)
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=token' % self.get_client().client_id)
|
||||
response = self.client.get(self.auth_url2())
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
def test_authorization_requires_a_valid_redirect_uri(self):
|
||||
self.login()
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code&redirect_uri=%s' % (
|
||||
self.get_client().client_id,
|
||||
self.get_client().redirect_uri + '-invalid'))
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue(escape(u"The requested redirect didn't match the client settings.") in response.content)
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code&redirect_uri=%s' % (
|
||||
self.get_client().client_id,
|
||||
self.get_client().redirect_uri))
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
def test_authorization_requires_a_valid_scope(self):
|
||||
self.login()
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code&scope=invalid+invalid2' % self.get_client().client_id)
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue(escape(u"'invalid' is not a valid scope.") in response.content)
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code&scope=%s' % (
|
||||
self.get_client().client_id,
|
||||
constants.SCOPES[0][1]))
|
||||
response = self.client.get(self.auth_url2())
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
def test_authorization_is_not_granted(self):
|
||||
self.login()
|
||||
|
||||
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code' % self.get_client().client_id)
|
||||
response = self.client.get(self.auth_url2())
|
||||
|
||||
response = self.client.post(self.auth_url2(), {'authorize': False, 'scope': constants.SCOPES[0][1]})
|
||||
self.assertEqual(302, response.status_code, response.content)
|
||||
self.assertTrue(self.redirect_url() in response['Location'])
|
||||
|
||||
response = self.client.get(self.redirect_url())
|
||||
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.assertTrue('error=access_denied' in response['Location'])
|
||||
self.assertFalse('code' in response['Location'])
|
||||
|
||||
def test_authorization_is_granted(self):
|
||||
self.login()
|
||||
|
||||
self._login_and_authorize()
|
||||
|
||||
response = self.client.get(self.redirect_url())
|
||||
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.assertFalse('error' in response['Location'])
|
||||
self.assertTrue('code' in response['Location'])
|
||||
|
||||
def test_preserving_the_state_variable(self):
|
||||
self.login()
|
||||
|
||||
self._login_and_authorize()
|
||||
|
||||
response = self.client.get(self.redirect_url())
|
||||
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.assertFalse('error' in response['Location'])
|
||||
self.assertTrue('code' in response['Location'])
|
||||
self.assertTrue('state=abc' in response['Location'])
|
||||
|
||||
def test_redirect_requires_valid_data(self):
|
||||
self.login()
|
||||
response = self.client.get(self.redirect_url())
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
||||
|
||||
class AccessTokenTest(BaseOAuth2TestCase):
|
||||
fixtures = ['test_oauth2.json']
|
||||
|
||||
def test_access_token_get_expire_delta_value(self):
|
||||
user = self.get_user()
|
||||
client = self.get_client()
|
||||
token = AccessToken.objects.create(user=user, client=client)
|
||||
now = date_now()
|
||||
default_expiration_timedelta = constants.EXPIRE_DELTA
|
||||
current_expiration_timedelta = datetime.timedelta(seconds=token.get_expire_delta(reference=now))
|
||||
self.assertTrue(abs(current_expiration_timedelta - default_expiration_timedelta) <= datetime.timedelta(seconds=1))
|
||||
|
||||
def test_fetching_access_token_with_invalid_client(self):
|
||||
self.login()
|
||||
self._login_and_authorize()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': self.get_client().client_id + '123',
|
||||
'client_secret': self.get_client().client_secret, })
|
||||
|
||||
self.assertEqual(400, response.status_code, response.content)
|
||||
self.assertEqual('invalid_client', json.loads(response.content)['error'])
|
||||
|
||||
def test_fetching_access_token_with_invalid_grant(self):
|
||||
self.login()
|
||||
self._login_and_authorize()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
'code': '123'})
|
||||
|
||||
self.assertEqual(400, response.status_code, response.content)
|
||||
self.assertEqual('invalid_grant', json.loads(response.content)['error'])
|
||||
|
||||
def _login_authorize_get_token(self):
|
||||
required_props = ['access_token', 'token_type']
|
||||
|
||||
self.login()
|
||||
self._login_and_authorize()
|
||||
|
||||
response = self.client.get(self.redirect_url())
|
||||
query = QueryDict(urlparse.urlparse(response['Location']).query)
|
||||
code = query['code']
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
'code': code})
|
||||
|
||||
self.assertEqual(200, response.status_code, response.content)
|
||||
|
||||
token = json.loads(response.content)
|
||||
|
||||
for prop in required_props:
|
||||
self.assertIn(prop, token, "Access token response missing "
|
||||
"required property: %s" % prop)
|
||||
|
||||
return token
|
||||
|
||||
def test_fetching_access_token_with_valid_grant(self):
|
||||
self._login_authorize_get_token()
|
||||
|
||||
def test_fetching_access_token_with_invalid_grant_type(self):
|
||||
self.login()
|
||||
self._login_and_authorize()
|
||||
response = self.client.get(self.redirect_url())
|
||||
|
||||
query = QueryDict(urlparse.urlparse(response['Location']).query)
|
||||
code = query['code']
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'invalid_grant_type',
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
'code': code
|
||||
})
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual('unsupported_grant_type', json.loads(response.content)['error'],
|
||||
response.content)
|
||||
|
||||
def test_fetching_single_access_token(self):
|
||||
constants.SINGLE_ACCESS_TOKEN = True
|
||||
|
||||
result1 = self._login_authorize_get_token()
|
||||
result2 = self._login_authorize_get_token()
|
||||
|
||||
self.assertEqual(result1['access_token'], result2['access_token'])
|
||||
|
||||
constants.SINGLE_ACCESS_TOKEN = False
|
||||
|
||||
def test_fetching_single_access_token_after_refresh(self):
|
||||
constants.SINGLE_ACCESS_TOKEN = True
|
||||
|
||||
token = self._login_authorize_get_token()
|
||||
|
||||
self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': token['refresh_token'],
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
})
|
||||
|
||||
new_token = self._login_authorize_get_token()
|
||||
self.assertNotEqual(token['access_token'], new_token['access_token'])
|
||||
|
||||
constants.SINGLE_ACCESS_TOKEN = False
|
||||
|
||||
def test_fetching_access_token_multiple_times(self):
|
||||
self._login_authorize_get_token()
|
||||
code = self.get_grant().code
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
'code': code})
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual('invalid_grant', json.loads(response.content)['error'])
|
||||
|
||||
def test_escalating_the_scope(self):
|
||||
self.login()
|
||||
self._login_and_authorize()
|
||||
code = self.get_grant().code
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
'code': code,
|
||||
'scope': 'read write'})
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual('invalid_scope', json.loads(response.content)['error'])
|
||||
|
||||
def test_refreshing_an_access_token(self):
|
||||
token = self._login_authorize_get_token()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': token['refresh_token'],
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
})
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': token['refresh_token'],
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
})
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual('invalid_grant', json.loads(response.content)['error'],
|
||||
response.content)
|
||||
|
||||
def test_password_grant_public(self):
|
||||
c = self.get_client()
|
||||
c.client_type = 1 # public
|
||||
c.save()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'password',
|
||||
'client_id': c.client_id,
|
||||
# No secret needed
|
||||
'username': self.get_user().username,
|
||||
'password': self.get_password(),
|
||||
})
|
||||
|
||||
self.assertEqual(200, response.status_code, response.content)
|
||||
self.assertNotIn('refresh_token', json.loads(response.content))
|
||||
expires_in = json.loads(response.content)['expires_in']
|
||||
expires_in_days = round(expires_in / (60.0 * 60.0 * 24.0))
|
||||
self.assertEqual(expires_in_days, constants.EXPIRE_DELTA_PUBLIC.days)
|
||||
|
||||
def test_password_grant_confidential(self):
|
||||
c = self.get_client()
|
||||
c.client_type = 0 # confidential
|
||||
c.save()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'password',
|
||||
'client_id': c.client_id,
|
||||
'client_secret': c.client_secret,
|
||||
'username': self.get_user().username,
|
||||
'password': self.get_password(),
|
||||
})
|
||||
|
||||
self.assertEqual(200, response.status_code, response.content)
|
||||
self.assertTrue(json.loads(response.content)['refresh_token'])
|
||||
|
||||
def test_password_grant_confidential_no_secret(self):
|
||||
c = self.get_client()
|
||||
c.client_type = 0 # confidential
|
||||
c.save()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'password',
|
||||
'client_id': c.client_id,
|
||||
'username': self.get_user().username,
|
||||
'password': self.get_password(),
|
||||
})
|
||||
|
||||
self.assertEqual('invalid_client', json.loads(response.content)['error'])
|
||||
|
||||
def test_password_grant_invalid_password_public(self):
|
||||
c = self.get_client()
|
||||
c.client_type = 1 # public
|
||||
c.save()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'password',
|
||||
'client_id': c.client_id,
|
||||
'username': self.get_user().username,
|
||||
'password': self.get_password() + 'invalid',
|
||||
})
|
||||
|
||||
self.assertEqual(400, response.status_code, response.content)
|
||||
self.assertEqual('invalid_client', json.loads(response.content)['error'])
|
||||
|
||||
def test_password_grant_invalid_password_confidential(self):
|
||||
c = self.get_client()
|
||||
c.client_type = 0 # confidential
|
||||
c.save()
|
||||
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'password',
|
||||
'client_id': c.client_id,
|
||||
'client_secret': c.client_secret,
|
||||
'username': self.get_user().username,
|
||||
'password': self.get_password() + 'invalid',
|
||||
})
|
||||
|
||||
self.assertEqual(400, response.status_code, response.content)
|
||||
self.assertEqual('invalid_grant', json.loads(response.content)['error'])
|
||||
|
||||
def test_access_token_response_valid_token_type(self):
|
||||
token = self._login_authorize_get_token()
|
||||
self.assertEqual(token['token_type'], constants.TOKEN_TYPE, token)
|
||||
|
||||
|
||||
class AuthBackendTest(BaseOAuth2TestCase):
|
||||
fixtures = ['test_oauth2']
|
||||
|
||||
def test_basic_client_backend(self):
|
||||
request = type('Request', (object,), {'META': {}})()
|
||||
request.META['HTTP_AUTHORIZATION'] = "Basic " + "{0}:{1}".format(
|
||||
self.get_client().client_id,
|
||||
self.get_client().client_secret).encode('base64')
|
||||
|
||||
self.assertEqual(BasicClientBackend().authenticate(request).id,
|
||||
2, "Didn't return the right client.")
|
||||
|
||||
def test_request_params_client_backend(self):
|
||||
request = type('Request', (object,), {'REQUEST': {}})()
|
||||
|
||||
request.REQUEST['client_id'] = self.get_client().client_id
|
||||
request.REQUEST['client_secret'] = self.get_client().client_secret
|
||||
|
||||
self.assertEqual(RequestParamsClientBackend().authenticate(request).id,
|
||||
2, "Didn't return the right client.'")
|
||||
|
||||
def test_access_token_backend(self):
|
||||
user = self.get_user()
|
||||
client = self.get_client()
|
||||
backend = AccessTokenBackend()
|
||||
token = AccessToken.objects.create(user=user, client=client)
|
||||
authenticated = backend.authenticate(access_token=token.token,
|
||||
client=client)
|
||||
|
||||
self.assertIsNotNone(authenticated)
|
||||
|
||||
|
||||
class EnforceSecureTest(BaseOAuth2TestCase):
|
||||
fixtures = ['test_oauth2']
|
||||
|
||||
def setUp(self):
|
||||
constants.ENFORCE_SECURE = True
|
||||
|
||||
def tearDown(self):
|
||||
constants.ENFORCE_SECURE = False
|
||||
|
||||
def test_authorization_enforces_SSL(self):
|
||||
self.login()
|
||||
|
||||
response = self.client.get(self.auth_url())
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue("A secure connection is required." in response.content)
|
||||
|
||||
def test_access_token_enforces_SSL(self):
|
||||
response = self.client.post(self.access_token_url(), {})
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertTrue("A secure connection is required." in response.content)
|
||||
|
||||
|
||||
class ClientFormTest(TestCase):
|
||||
def test_client_form(self):
|
||||
form = ClientForm({'name': 'TestName', 'url': 'http://127.0.0.1:8000',
|
||||
'redirect_uri': 'http://localhost:8000/'})
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
form = ClientForm({
|
||||
'name': 'TestName',
|
||||
'url': 'http://127.0.0.1:8000',
|
||||
'redirect_uri': 'http://localhost:8000/',
|
||||
'client_type': constants.CLIENT_TYPES[0][0]})
|
||||
self.assertTrue(form.is_valid())
|
||||
form.save()
|
||||
|
||||
|
||||
class ScopeTest(TestCase):
|
||||
def setUp(self):
|
||||
self._scopes = constants.SCOPES
|
||||
constants.SCOPES = constants.DEFAULT_SCOPES
|
||||
|
||||
def tearDown(self):
|
||||
constants.SCOPES = self._scopes
|
||||
|
||||
def test_get_scope_names(self):
|
||||
names = scope.to_names(constants.READ)
|
||||
self.assertEqual('read', ' '.join(names))
|
||||
|
||||
names = scope.names(constants.READ_WRITE)
|
||||
names.sort()
|
||||
|
||||
self.assertEqual('read read+write write', ' '.join(names))
|
||||
|
||||
def test_get_scope_ints(self):
|
||||
self.assertEqual(constants.READ, scope.to_int('read'))
|
||||
self.assertEqual(constants.WRITE, scope.to_int('write'))
|
||||
self.assertEqual(constants.READ_WRITE, scope.to_int('read', 'write'))
|
||||
self.assertEqual(0, scope.to_int('invalid'))
|
||||
self.assertEqual(1, scope.to_int('invalid', default=1))
|
||||
|
||||
def test_template_filter(self):
|
||||
names = scopes(constants.READ)
|
||||
self.assertEqual('read', ' '.join(names))
|
||||
|
||||
names = scope.names(constants.READ_WRITE)
|
||||
names.sort()
|
||||
|
||||
self.assertEqual('read read+write write', ' '.join(names))
|
||||
|
||||
|
||||
class DeleteExpiredTest(BaseOAuth2TestCase):
|
||||
fixtures = ['test_oauth2']
|
||||
|
||||
def setUp(self):
|
||||
self._delete_expired = constants.DELETE_EXPIRED
|
||||
constants.DELETE_EXPIRED = True
|
||||
|
||||
def tearDown(self):
|
||||
constants.DELETE_EXPIRED = self._delete_expired
|
||||
|
||||
def test_clear_expired(self):
|
||||
self.login()
|
||||
|
||||
self._login_and_authorize()
|
||||
|
||||
response = self.client.get(self.redirect_url())
|
||||
|
||||
self.assertEqual(302, response.status_code)
|
||||
location = response['Location']
|
||||
self.assertFalse('error' in location)
|
||||
self.assertTrue('code' in location)
|
||||
|
||||
# verify that Grant with code exists
|
||||
code = urlparse.parse_qs(location)['code'][0]
|
||||
self.assertTrue(Grant.objects.filter(code=code).exists())
|
||||
|
||||
# use the code/grant
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
'code': code})
|
||||
self.assertEquals(200, response.status_code)
|
||||
token = json.loads(response.content)
|
||||
self.assertTrue('access_token' in token)
|
||||
access_token = token['access_token']
|
||||
self.assertTrue('refresh_token' in token)
|
||||
refresh_token = token['refresh_token']
|
||||
|
||||
# make sure the grant is gone
|
||||
self.assertFalse(Grant.objects.filter(code=code).exists())
|
||||
# and verify that the AccessToken and RefreshToken exist
|
||||
self.assertTrue(AccessToken.objects.filter(token=access_token)
|
||||
.exists())
|
||||
self.assertTrue(RefreshToken.objects.filter(token=refresh_token)
|
||||
.exists())
|
||||
|
||||
# refresh the token
|
||||
response = self.client.post(self.access_token_url(), {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': token['refresh_token'],
|
||||
'client_id': self.get_client().client_id,
|
||||
'client_secret': self.get_client().client_secret,
|
||||
})
|
||||
self.assertEqual(200, response.status_code)
|
||||
token = json.loads(response.content)
|
||||
self.assertTrue('access_token' in token)
|
||||
self.assertNotEquals(access_token, token['access_token'])
|
||||
self.assertTrue('refresh_token' in token)
|
||||
self.assertNotEquals(refresh_token, token['refresh_token'])
|
||||
|
||||
# make sure the orig AccessToken and RefreshToken are gone
|
||||
self.assertFalse(AccessToken.objects.filter(token=access_token)
|
||||
.exists())
|
||||
self.assertFalse(RefreshToken.objects.filter(token=refresh_token)
|
||||
.exists())
|
|
@ -1,55 +0,0 @@
|
|||
"""
|
||||
The default implementation of the OAuth provider includes two public endpoints
|
||||
that are meant for client (as defined in :rfc:`1`) interaction.
|
||||
|
||||
.. attribute:: ^authorize/$
|
||||
|
||||
This is the URL where a client should redirect a user to for authorization.
|
||||
|
||||
This endpoint expects the parameters defined in :rfc:`4.1.1` and returns
|
||||
responses as defined in :rfc:`4.1.2` and :rfc:`4.1.2.1`.
|
||||
|
||||
.. attribute:: ^access_token/$
|
||||
|
||||
This is the URL where a client exchanges a grant for an access tokens.
|
||||
|
||||
This endpoint expects different parameters depending on the grant type:
|
||||
|
||||
* Access tokens: :rfc:`4.1.3`
|
||||
* Refresh tokens: :rfc:`6`
|
||||
* Password grant: :rfc:`4.3.2`
|
||||
|
||||
This endpoint returns responses depending on the grant type:
|
||||
|
||||
* Access tokens: :rfc:`4.1.4` and :rfc:`5.1`
|
||||
* Refresh tokens: :rfc:`4.1.4` and :rfc:`5.1`
|
||||
* Password grant: :rfc:`5.1`
|
||||
|
||||
To override, remove or add grant types, override the appropriate methods on
|
||||
:class:`provider.views.AccessToken` and / or
|
||||
:class:`provider.oauth2.views.AccessTokenView`.
|
||||
|
||||
Errors are outlined in :rfc:`5.2`.
|
||||
|
||||
"""
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from ..compat.urls import *
|
||||
from .views import Authorize, Redirect, Capture, AccessTokenView
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url('^authorize/?$',
|
||||
login_required(Capture.as_view()),
|
||||
name='capture'),
|
||||
url('^authorize/confirm/?$',
|
||||
login_required(Authorize.as_view()),
|
||||
name='authorize'),
|
||||
url('^redirect/?$',
|
||||
login_required(Redirect.as_view()),
|
||||
name='redirect'),
|
||||
url('^access_token/?$',
|
||||
csrf_exempt(AccessTokenView.as_view()),
|
||||
name='access_token'),
|
||||
)
|
|
@ -1,139 +0,0 @@
|
|||
from datetime import timedelta
|
||||
from django.core.urlresolvers import reverse
|
||||
from .. import constants
|
||||
from ..views import Capture, Authorize, Redirect
|
||||
from ..views import AccessToken as AccessTokenView, OAuthError
|
||||
from ..utils import now
|
||||
from .forms import AuthorizationRequestForm, AuthorizationForm
|
||||
from .forms import PasswordGrantForm, RefreshTokenGrantForm
|
||||
from .forms import AuthorizationCodeGrantForm
|
||||
from .models import Client, RefreshToken, AccessToken
|
||||
from .backends import BasicClientBackend, RequestParamsClientBackend, PublicPasswordBackend
|
||||
|
||||
|
||||
class Capture(Capture):
|
||||
"""
|
||||
Implementation of :class:`provider.views.Capture`.
|
||||
"""
|
||||
def get_redirect_url(self, request):
|
||||
return reverse('oauth2:authorize')
|
||||
|
||||
|
||||
class Authorize(Authorize):
|
||||
"""
|
||||
Implementation of :class:`provider.views.Authorize`.
|
||||
"""
|
||||
def get_request_form(self, client, data):
|
||||
return AuthorizationRequestForm(data, client=client)
|
||||
|
||||
def get_authorization_form(self, request, client, data, client_data):
|
||||
return AuthorizationForm(data)
|
||||
|
||||
def get_client(self, client_id):
|
||||
try:
|
||||
return Client.objects.get(client_id=client_id)
|
||||
except Client.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_redirect_url(self, request):
|
||||
return reverse('oauth2:redirect')
|
||||
|
||||
def save_authorization(self, request, client, form, client_data):
|
||||
|
||||
grant = form.save(commit=False)
|
||||
|
||||
if grant is None:
|
||||
return None
|
||||
|
||||
grant.user = request.user
|
||||
grant.client = client
|
||||
grant.redirect_uri = client_data.get('redirect_uri', '')
|
||||
grant.save()
|
||||
return grant.code
|
||||
|
||||
|
||||
class Redirect(Redirect):
|
||||
"""
|
||||
Implementation of :class:`provider.views.Redirect`
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AccessTokenView(AccessTokenView):
|
||||
"""
|
||||
Implementation of :class:`provider.views.AccessToken`.
|
||||
|
||||
.. note:: This implementation does provide all default grant types defined
|
||||
in :attr:`provider.views.AccessToken.grant_types`. If you
|
||||
wish to disable any, you can override the :meth:`get_handler` method
|
||||
*or* the :attr:`grant_types` list.
|
||||
"""
|
||||
authentication = (
|
||||
BasicClientBackend,
|
||||
RequestParamsClientBackend,
|
||||
PublicPasswordBackend,
|
||||
)
|
||||
|
||||
def get_authorization_code_grant(self, request, data, client):
|
||||
form = AuthorizationCodeGrantForm(data, client=client)
|
||||
if not form.is_valid():
|
||||
raise OAuthError(form.errors)
|
||||
return form.cleaned_data.get('grant')
|
||||
|
||||
def get_refresh_token_grant(self, request, data, client):
|
||||
form = RefreshTokenGrantForm(data, client=client)
|
||||
if not form.is_valid():
|
||||
raise OAuthError(form.errors)
|
||||
return form.cleaned_data.get('refresh_token')
|
||||
|
||||
def get_password_grant(self, request, data, client):
|
||||
form = PasswordGrantForm(data, client=client)
|
||||
if not form.is_valid():
|
||||
raise OAuthError(form.errors)
|
||||
return form.cleaned_data
|
||||
|
||||
def get_access_token(self, request, user, scope, client):
|
||||
try:
|
||||
# Attempt to fetch an existing access token.
|
||||
at = AccessToken.objects.get(user=user, client=client,
|
||||
scope=scope, expires__gt=now())
|
||||
except AccessToken.DoesNotExist:
|
||||
# None found... make a new one!
|
||||
at = self.create_access_token(request, user, scope, client)
|
||||
self.create_refresh_token(request, user, scope, at, client)
|
||||
return at
|
||||
|
||||
def create_access_token(self, request, user, scope, client):
|
||||
return AccessToken.objects.create(
|
||||
user=user,
|
||||
client=client,
|
||||
scope=scope
|
||||
)
|
||||
|
||||
def create_refresh_token(self, request, user, scope, access_token, client):
|
||||
return RefreshToken.objects.create(
|
||||
user=user,
|
||||
access_token=access_token,
|
||||
client=client
|
||||
)
|
||||
|
||||
def invalidate_grant(self, grant):
|
||||
if constants.DELETE_EXPIRED:
|
||||
grant.delete()
|
||||
else:
|
||||
grant.expires = now() - timedelta(days=1)
|
||||
grant.save()
|
||||
|
||||
def invalidate_refresh_token(self, rt):
|
||||
if constants.DELETE_EXPIRED:
|
||||
rt.delete()
|
||||
else:
|
||||
rt.expired = True
|
||||
rt.save()
|
||||
|
||||
def invalidate_access_token(self, at):
|
||||
if constants.DELETE_EXPIRED:
|
||||
at.delete()
|
||||
else:
|
||||
at.expires = now() - timedelta(days=1)
|
||||
at.save()
|
|
@ -1,104 +0,0 @@
|
|||
"""
|
||||
Default scope implementation relying on bit shifting. See
|
||||
:attr:`provider.constants.SCOPES` for the list of available scopes.
|
||||
|
||||
Scopes can be combined, such as ``"read write"``. Note that a single
|
||||
``"write"`` scope is *not* the same as ``"read write"``.
|
||||
|
||||
See :class:`provider.scope.to_int` on how scopes are combined.
|
||||
"""
|
||||
|
||||
from .constants import SCOPES
|
||||
|
||||
SCOPE_NAMES = [(name, name) for (value, name) in SCOPES]
|
||||
SCOPE_NAME_DICT = dict([(name, value) for (value, name) in SCOPES])
|
||||
SCOPE_VALUE_DICT = dict([(value, name) for (value, name) in SCOPES])
|
||||
|
||||
|
||||
def check(wants, has):
|
||||
"""
|
||||
Check if a desired scope ``wants`` is part of an available scope ``has``.
|
||||
|
||||
Returns ``False`` if not, return ``True`` if yes.
|
||||
|
||||
:example:
|
||||
|
||||
If a list of scopes such as
|
||||
|
||||
::
|
||||
|
||||
READ = 1 << 1
|
||||
WRITE = 1 << 2
|
||||
READ_WRITE = READ | WRITE
|
||||
|
||||
SCOPES = (
|
||||
(READ, 'read'),
|
||||
(WRITE, 'write'),
|
||||
(READ_WRITE, 'read+write'),
|
||||
)
|
||||
|
||||
is defined, we can check if a given scope is part of another:
|
||||
|
||||
::
|
||||
|
||||
>>> from provider import scope
|
||||
>>> scope.check(READ, READ)
|
||||
True
|
||||
>>> scope.check(WRITE, READ)
|
||||
False
|
||||
>>> scope.check(WRITE, WRITE)
|
||||
True
|
||||
>>> scope.check(READ, WRITE)
|
||||
False
|
||||
>>> scope.check(READ, READ_WRITE)
|
||||
True
|
||||
>>> scope.check(WRITE, READ_WRITE)
|
||||
True
|
||||
|
||||
"""
|
||||
if wants & has == 0:
|
||||
return False
|
||||
if wants & has < wants:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def to_names(scope):
|
||||
"""
|
||||
Returns a list of scope names as defined in
|
||||
:attr:`provider.constants.SCOPES` for a given scope integer.
|
||||
|
||||
>>> assert ['read', 'write'] == provider.scope.names(provider.constants.READ_WRITE)
|
||||
|
||||
"""
|
||||
return [
|
||||
name
|
||||
for (name, value) in SCOPE_NAME_DICT.iteritems()
|
||||
if check(value, scope)
|
||||
]
|
||||
|
||||
# Keep it compatible
|
||||
names = to_names
|
||||
|
||||
|
||||
def to_int(*names, **kwargs):
|
||||
"""
|
||||
Turns a list of scope names into an integer value.
|
||||
|
||||
::
|
||||
|
||||
>>> scope.to_int('read')
|
||||
2
|
||||
>>> scope.to_int('write')
|
||||
6
|
||||
>>> scope.to_int('read', 'write')
|
||||
6
|
||||
>>> scope.to_int('invalid')
|
||||
0
|
||||
>>> scope.to_int('invalid', default = 1)
|
||||
1
|
||||
|
||||
"""
|
||||
|
||||
return reduce(lambda prev, next: (prev | SCOPE_NAME_DICT.get(next, 0)),
|
||||
names, kwargs.pop('default', 0))
|
|
@ -1,35 +0,0 @@
|
|||
"""
|
||||
Custom Sphinx documentation module to link to parts of the OAuth2 draft.
|
||||
"""
|
||||
from docutils import nodes, utils
|
||||
|
||||
base_url = "http://tools.ietf.org/html/rfc6749"
|
||||
|
||||
def rfclink(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
"""Link to the OAuth2 draft.
|
||||
|
||||
Returns 2 part tuple containing list of nodes to insert into the
|
||||
document and a list of system messages. Both are allowed to be
|
||||
empty.
|
||||
|
||||
:param name: The role name used in the document.
|
||||
:param rawtext: The entire markup snippet, with role.
|
||||
:param text: The text marked with the role.
|
||||
:param lineno: The line number where rawtext appears in the input.
|
||||
:param inliner: The inliner instance that called us.
|
||||
:param options: Directive options for customization.
|
||||
:param content: The directive content for customization.
|
||||
"""
|
||||
|
||||
node = nodes.reference(rawtext, "Section " + text, refuri="%s#section-%s" % (base_url, text))
|
||||
|
||||
return [node], []
|
||||
|
||||
def setup(app):
|
||||
"""
|
||||
Install the plugin.
|
||||
|
||||
:param app: Sphinx application context.
|
||||
"""
|
||||
app.add_role('rfc', rfclink)
|
||||
return
|
|
@ -1,37 +0,0 @@
|
|||
{% load scope %}
|
||||
{% load url from future %}
|
||||
{% block content %}
|
||||
{% if not error %}
|
||||
<p>{{ client.name }} would like to access your data with the following permissions:</p>
|
||||
<ul>
|
||||
{% for permission in oauth_data.scope|scopes %}
|
||||
<li>
|
||||
{% if permission == "read" %}
|
||||
Read your data
|
||||
{% else %}
|
||||
Write your data
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form method="post" action="{% url "oauth2:authorize" %}">
|
||||
{% csrf_token %}
|
||||
{{ form.errors }}
|
||||
{{ form.non_field_errors }}
|
||||
<fieldset>
|
||||
<div style="display: none;" />
|
||||
<select type="select" name="scope" multiple="multiple">
|
||||
{% for scope in oauth_data.scope|scopes %}
|
||||
<option value="{{ scope }}" selected="selected">{{ scope }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit" class="btn login large danger" value="Cancel" />
|
||||
<input type="submit" class="btn login large primary" name="authorize" value="Authorize" />
|
||||
</fieldset>
|
||||
</form>
|
||||
{% else %}
|
||||
{{ error }}
|
||||
{{ error_description }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,13 +0,0 @@
|
|||
from django import template
|
||||
from .. import scope
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def scopes(scope_int):
|
||||
"""
|
||||
Wrapper around :attr:`provider.scope.names` to turn an int into a list
|
||||
of scope names in templates.
|
||||
"""
|
||||
return scope.to_names(scope_int)
|
|
@ -1,35 +0,0 @@
|
|||
"""
|
||||
Test cases for functionality provided by the provider.utils module
|
||||
"""
|
||||
|
||||
from datetime import datetime, time, date
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from .. import utils
|
||||
|
||||
|
||||
class UtilsTestCase(TestCase):
|
||||
def test_serialization(self):
|
||||
class SomeModel(models.Model):
|
||||
dt = models.DateTimeField()
|
||||
t = models.TimeField()
|
||||
d = models.DateField()
|
||||
instance = SomeModel(dt=datetime.now(),
|
||||
d=date.today(),
|
||||
t=datetime.now().time())
|
||||
instance.nonfield = 'hello'
|
||||
data = utils.serialize_instance(instance)
|
||||
instance2 = utils.deserialize_instance(SomeModel, data)
|
||||
self.assertEqual(instance.nonfield, instance2.nonfield)
|
||||
self.assertEqual(instance.d, instance2.d)
|
||||
self.assertEqual(instance.dt.date(), instance2.dt.date())
|
||||
for t1, t2 in [(instance.t, instance2.t),
|
||||
(instance.dt.time(), instance2.dt.time())]:
|
||||
self.assertEqual(t1.hour, t2.hour)
|
||||
self.assertEqual(t1.minute, t2.minute)
|
||||
self.assertEqual(t1.second, t2.second)
|
||||
# AssertionError:
|
||||
# datetime.time(10, 6, 28, 705776) !=
|
||||
# datetime.time(10, 6, 28, 705000)
|
||||
self.assertEqual(int(t1.microsecond/1000),
|
||||
int(t2.microsecond/1000))
|
|
@ -1,100 +0,0 @@
|
|||
import hashlib
|
||||
import shortuuid
|
||||
from datetime import datetime, tzinfo
|
||||
from django.conf import settings
|
||||
from django.utils import dateparse
|
||||
from django.db.models.fields import (DateTimeField, DateField,
|
||||
EmailField, TimeField,
|
||||
FieldDoesNotExist)
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from .constants import EXPIRE_DELTA, EXPIRE_DELTA_PUBLIC, EXPIRE_CODE_DELTA
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImporError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
from django.utils import timezone
|
||||
except ImportError:
|
||||
timezone = None
|
||||
|
||||
def now():
|
||||
if timezone:
|
||||
return timezone.now()
|
||||
else:
|
||||
# Django 1.3 compatibility
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def short_token():
|
||||
"""
|
||||
Generate a hash that can be used as an application identifier
|
||||
"""
|
||||
hash = hashlib.sha1(shortuuid.uuid())
|
||||
hash.update(settings.SECRET_KEY)
|
||||
return hash.hexdigest()[::2]
|
||||
|
||||
|
||||
def long_token():
|
||||
"""
|
||||
Generate a hash that can be used as an application secret
|
||||
"""
|
||||
hash = hashlib.sha1(shortuuid.uuid())
|
||||
hash.update(settings.SECRET_KEY)
|
||||
return hash.hexdigest()
|
||||
|
||||
|
||||
def get_token_expiry(public=True):
|
||||
"""
|
||||
Return a datetime object indicating when an access token should expire.
|
||||
Can be customized by setting :attr:`settings.OAUTH_EXPIRE_DELTA` to a
|
||||
:attr:`datetime.timedelta` object.
|
||||
"""
|
||||
if public:
|
||||
return now() + EXPIRE_DELTA_PUBLIC
|
||||
else:
|
||||
return now() + EXPIRE_DELTA
|
||||
|
||||
|
||||
def get_code_expiry():
|
||||
"""
|
||||
Return a datetime object indicating when an authorization code should
|
||||
expire.
|
||||
Can be customized by setting :attr:`settings.OAUTH_EXPIRE_CODE_DELTA` to a
|
||||
:attr:`datetime.timedelta` object.
|
||||
"""
|
||||
return now() + EXPIRE_CODE_DELTA
|
||||
|
||||
|
||||
def serialize_instance(instance):
|
||||
"""
|
||||
Since Django 1.6 items added to the session are no longer pickled,
|
||||
but JSON encoded by default. We are storing partially complete models
|
||||
in the session (user, account, token, ...). We cannot use standard
|
||||
Django serialization, as these are models are not "complete" yet.
|
||||
Serialization will start complaining about missing relations et al.
|
||||
"""
|
||||
ret = dict([(k, v)
|
||||
for k, v in instance.__dict__.items()
|
||||
if not k.startswith('_')])
|
||||
return json.loads(json.dumps(ret, cls=DjangoJSONEncoder))
|
||||
|
||||
|
||||
def deserialize_instance(model, data={}):
|
||||
"Translate raw data into a model instance."
|
||||
ret = model()
|
||||
for k, v in data.items():
|
||||
if v is not None:
|
||||
try:
|
||||
f = model._meta.get_field(k)
|
||||
if isinstance(f, DateTimeField):
|
||||
v = dateparse.parse_datetime(v)
|
||||
elif isinstance(f, TimeField):
|
||||
v = dateparse.parse_time(v)
|
||||
elif isinstance(f, DateField):
|
||||
v = dateparse.parse_date(v)
|
||||
except FieldDoesNotExist:
|
||||
pass
|
||||
setattr(ret, k, v)
|
||||
return ret
|
|
@ -1,600 +0,0 @@
|
|||
import json
|
||||
import urlparse
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from oauth2.models import Client
|
||||
from . import constants, scope
|
||||
|
||||
|
||||
class OAuthError(Exception):
|
||||
"""
|
||||
Exception to throw inside any views defined in :attr:`provider.views`.
|
||||
|
||||
Any :attr:`OAuthError` thrown will be signalled to the API consumer.
|
||||
|
||||
:attr:`OAuthError` expects a dictionary as its first argument outlining the
|
||||
type of error that occured.
|
||||
|
||||
:example:
|
||||
|
||||
::
|
||||
|
||||
raise OAuthError({'error': 'invalid_request'})
|
||||
|
||||
The different types of errors are outlined in :rfc:`4.2.2.1` and
|
||||
:rfc:`5.2`.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class OAuthView(TemplateView):
|
||||
"""
|
||||
Base class for any view dealing with the OAuth flow. This class overrides
|
||||
the dispatch method of :attr:`TemplateView` to add no-caching headers to
|
||||
every response as outlined in :rfc:`5.1`.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super(OAuthView, self).dispatch(request, *args, **kwargs)
|
||||
response['Cache-Control'] = 'no-store'
|
||||
response['Pragma'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
class Mixin(object):
|
||||
"""
|
||||
Mixin providing common methods required in the OAuth view defined in
|
||||
:attr:`provider.views`.
|
||||
"""
|
||||
def get_data(self, request, key='params'):
|
||||
"""
|
||||
Return stored data from the session store.
|
||||
|
||||
:param key: `str` The key under which the data was stored.
|
||||
"""
|
||||
return request.session.get('%s:%s' % (constants.SESSION_KEY, key))
|
||||
|
||||
def cache_data(self, request, data, key='params'):
|
||||
"""
|
||||
Cache data in the session store.
|
||||
|
||||
:param request: :attr:`django.http.HttpRequest`
|
||||
:param data: Arbitrary data to store.
|
||||
:param key: `str` The key under which to store the data.
|
||||
"""
|
||||
request.session['%s:%s' % (constants.SESSION_KEY, key)] = data
|
||||
|
||||
def clear_data(self, request):
|
||||
"""
|
||||
Clear all OAuth related data from the session store.
|
||||
"""
|
||||
for key in request.session.keys():
|
||||
if key.startswith(constants.SESSION_KEY):
|
||||
del request.session[key]
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Authenticate a client against all the backends configured in
|
||||
:attr:`authentication`.
|
||||
"""
|
||||
for backend in self.authentication:
|
||||
client = backend().authenticate(request)
|
||||
if client is not None:
|
||||
return client
|
||||
return None
|
||||
|
||||
|
||||
class Capture(OAuthView, Mixin):
|
||||
"""
|
||||
As stated in section :rfc:`3.1.2.5` this view captures all the request
|
||||
parameters and redirects to another URL to avoid any leakage of request
|
||||
parameters to potentially harmful JavaScripts.
|
||||
|
||||
This application assumes that whatever web-server is used as front-end will
|
||||
handle SSL transport.
|
||||
|
||||
If you want strict enforcement of secure communication at application
|
||||
level, set :attr:`settings.OAUTH_ENFORCE_SECURE` to ``True``.
|
||||
|
||||
The actual implementation is required to override :meth:`get_redirect_url`.
|
||||
"""
|
||||
template_name = 'provider/authorize.html'
|
||||
|
||||
def get_redirect_url(self, request):
|
||||
"""
|
||||
Return a redirect to a URL where the resource owner (see :rfc:`1`)
|
||||
authorizes the client (also :rfc:`1`).
|
||||
|
||||
:return: :class:`django.http.HttpResponseRedirect`
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def handle(self, request, data):
|
||||
self.cache_data(request, data)
|
||||
|
||||
if constants.ENFORCE_SECURE and not request.is_secure():
|
||||
return self.render_to_response({'error': 'access_denied',
|
||||
'error_description': _("A secure connection is required."),
|
||||
'next': None},
|
||||
status=400)
|
||||
|
||||
return HttpResponseRedirect(self.get_redirect_url(request))
|
||||
|
||||
def get(self, request):
|
||||
return self.handle(request, request.GET)
|
||||
|
||||
def post(self, request):
|
||||
return self.handle(request, request.POST)
|
||||
|
||||
|
||||
class Authorize(OAuthView, Mixin):
|
||||
"""
|
||||
View to handle the client authorization as outlined in :rfc:`4`.
|
||||
Implementation must override a set of methods:
|
||||
|
||||
* :attr:`get_redirect_url`
|
||||
* :attr:`get_request_form`
|
||||
* :attr:`get_authorization_form`
|
||||
* :attr:`get_client`
|
||||
* :attr:`save_authorization`
|
||||
|
||||
:attr:`Authorize` renders the ``provider/authorize.html`` template to
|
||||
display the authorization form.
|
||||
|
||||
On successful authorization, it redirects the user back to the defined
|
||||
client callback as defined in :rfc:`4.1.2`.
|
||||
|
||||
On authorization fail :attr:`Authorize` displays an error message to the
|
||||
user with a modified redirect URL to the callback including the error
|
||||
and possibly description of the error as defined in :rfc:`4.1.2.1`.
|
||||
"""
|
||||
template_name = 'provider/authorize.html'
|
||||
|
||||
def get_redirect_url(self, request):
|
||||
"""
|
||||
:return: ``str`` - The client URL to display in the template after
|
||||
authorization succeeded or failed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_request_form(self, client, data):
|
||||
"""
|
||||
Return a form that is capable of validating the request data captured
|
||||
by the :class:`Capture` view.
|
||||
The form must accept a keyword argument ``client``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_authorization_form(self, request, client, data, client_data):
|
||||
"""
|
||||
Return a form that is capable of authorizing the client to the resource
|
||||
owner.
|
||||
|
||||
:return: :attr:`django.forms.Form`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_client(self, client_id):
|
||||
"""
|
||||
Return a client object from a given client identifier. Return ``None``
|
||||
if no client is found. An error will be displayed to the resource owner
|
||||
and presented to the client upon the final redirect.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save_authorization(self, request, client, form, client_data):
|
||||
"""
|
||||
Save the authorization that the user granted to the client, involving
|
||||
the creation of a time limited authorization code as outlined in
|
||||
:rfc:`4.1.2`.
|
||||
|
||||
Should return ``None`` in case authorization is not granted.
|
||||
Should return a string representing the authorization code grant.
|
||||
|
||||
:return: ``None``, ``str``
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _validate_client(self, request, data):
|
||||
"""
|
||||
:return: ``tuple`` - ``(client or False, data or error)``
|
||||
"""
|
||||
client = self.get_client(data.get('client_id'))
|
||||
|
||||
if client is None:
|
||||
raise OAuthError({
|
||||
'error': 'unauthorized_client',
|
||||
'error_description': _("An unauthorized client tried to access"
|
||||
" your resources.")
|
||||
})
|
||||
|
||||
form = self.get_request_form(client, data)
|
||||
|
||||
if not form.is_valid():
|
||||
raise OAuthError(form.errors)
|
||||
|
||||
return client, form.cleaned_data
|
||||
|
||||
def error_response(self, request, error, **kwargs):
|
||||
"""
|
||||
Return an error to be displayed to the resource owner if anything goes
|
||||
awry. Errors can include invalid clients, authorization denials and
|
||||
other edge cases such as a wrong ``redirect_uri`` in the authorization
|
||||
request.
|
||||
|
||||
:param request: :attr:`django.http.HttpRequest`
|
||||
:param error: ``dict``
|
||||
The different types of errors are outlined in :rfc:`4.2.2.1`
|
||||
"""
|
||||
ctx = {}
|
||||
ctx.update(error)
|
||||
|
||||
# If we got a malicious redirect_uri or client_id, remove all the
|
||||
# cached data and tell the resource owner. We will *not* redirect back
|
||||
# to the URL.
|
||||
|
||||
if error['error'] in ['redirect_uri', 'unauthorized_client']:
|
||||
ctx.update(next='/')
|
||||
return self.render_to_response(ctx, **kwargs)
|
||||
|
||||
ctx.update(next=self.get_redirect_url(request))
|
||||
|
||||
return self.render_to_response(ctx, **kwargs)
|
||||
|
||||
def handle(self, request, post_data=None):
|
||||
data = self.get_data(request)
|
||||
|
||||
if data is None:
|
||||
return self.error_response(request, {
|
||||
'error': 'expired_authorization',
|
||||
'error_description': _('Authorization session has expired.')})
|
||||
|
||||
try:
|
||||
client, data = self._validate_client(request, data)
|
||||
except OAuthError, e:
|
||||
return self.error_response(request, e.args[0], status=400)
|
||||
|
||||
authorization_form = self.get_authorization_form(request, client,
|
||||
post_data, data)
|
||||
|
||||
if not authorization_form.is_bound or not authorization_form.is_valid():
|
||||
return self.render_to_response({
|
||||
'client': client,
|
||||
'form': authorization_form,
|
||||
'oauth_data': data, })
|
||||
|
||||
code = self.save_authorization(request, client,
|
||||
authorization_form, data)
|
||||
|
||||
# be sure to serialize any objects that aren't natively json
|
||||
# serializable because these values are stored as session data
|
||||
self.cache_data(request, data)
|
||||
self.cache_data(request, code, "code")
|
||||
self.cache_data(request, client.serialize(), "client")
|
||||
|
||||
return HttpResponseRedirect(self.get_redirect_url(request))
|
||||
|
||||
def get(self, request):
|
||||
return self.handle(request, None)
|
||||
|
||||
def post(self, request):
|
||||
return self.handle(request, request.POST)
|
||||
|
||||
|
||||
class Redirect(OAuthView, Mixin):
|
||||
"""
|
||||
Redirect the user back to the client with the right query parameters set.
|
||||
This can be either parameters indicating success or parameters indicating
|
||||
an error.
|
||||
"""
|
||||
|
||||
def error_response(self, error, mimetype='application/json', status=400,
|
||||
**kwargs):
|
||||
"""
|
||||
Return an error response to the client with default status code of
|
||||
*400* stating the error as outlined in :rfc:`5.2`.
|
||||
"""
|
||||
return HttpResponse(json.dumps(error), content_type=mimetype,
|
||||
status=status, **kwargs)
|
||||
|
||||
def get(self, request):
|
||||
data = self.get_data(request)
|
||||
code = self.get_data(request, "code")
|
||||
error = self.get_data(request, "error")
|
||||
client = self.get_data(request, "client")
|
||||
|
||||
# client must be properly deserialized to become a valid instance
|
||||
client = Client.deserialize(client)
|
||||
|
||||
# this is an edge case that is caused by making a request with no data
|
||||
# it should only happen if this view is called manually, out of the
|
||||
# normal capture-authorize-redirect flow.
|
||||
if data is None or client is None:
|
||||
return self.error_response({
|
||||
'error': 'invalid_data',
|
||||
'error_description': _('Data has not been captured')})
|
||||
|
||||
redirect_uri = data.get('redirect_uri', None) or client.redirect_uri
|
||||
|
||||
parsed = urlparse.urlparse(redirect_uri)
|
||||
|
||||
query = QueryDict('', mutable=True)
|
||||
|
||||
if 'state' in data:
|
||||
query['state'] = data['state']
|
||||
|
||||
if error is not None:
|
||||
query.update(error)
|
||||
elif code is None:
|
||||
query['error'] = 'access_denied'
|
||||
else:
|
||||
query['code'] = code
|
||||
|
||||
parsed = parsed[:4] + (query.urlencode(), '')
|
||||
|
||||
redirect_uri = urlparse.ParseResult(*parsed).geturl()
|
||||
|
||||
self.clear_data(request)
|
||||
|
||||
return HttpResponseRedirect(redirect_uri)
|
||||
|
||||
|
||||
class AccessToken(OAuthView, Mixin):
|
||||
"""
|
||||
:attr:`AccessToken` handles creation and refreshing of access tokens.
|
||||
|
||||
Implementations must implement a number of methods:
|
||||
|
||||
* :attr:`get_authorization_code_grant`
|
||||
* :attr:`get_refresh_token_grant`
|
||||
* :attr:`get_password_grant`
|
||||
* :attr:`get_access_token`
|
||||
* :attr:`create_access_token`
|
||||
* :attr:`create_refresh_token`
|
||||
* :attr:`invalidate_grant`
|
||||
* :attr:`invalidate_access_token`
|
||||
* :attr:`invalidate_refresh_token`
|
||||
|
||||
The default implementation supports the grant types defined in
|
||||
:attr:`grant_types`.
|
||||
|
||||
According to :rfc:`4.4.2` this endpoint too must support secure
|
||||
communication. For strict enforcement of secure communication at
|
||||
application level set :attr:`settings.OAUTH_ENFORCE_SECURE` to ``True``.
|
||||
|
||||
According to :rfc:`3.2` we can only accept POST requests.
|
||||
|
||||
Returns with a status code of *400* in case of errors. *200* in case of
|
||||
success.
|
||||
"""
|
||||
|
||||
authentication = ()
|
||||
"""
|
||||
Authentication backends used to authenticate a particular client.
|
||||
"""
|
||||
|
||||
grant_types = ['authorization_code', 'refresh_token', 'password']
|
||||
"""
|
||||
The default grant types supported by this view.
|
||||
"""
|
||||
|
||||
def get_authorization_code_grant(self, request, data, client):
|
||||
"""
|
||||
Return the grant associated with this request or an error dict.
|
||||
|
||||
:return: ``tuple`` - ``(True or False, grant or error_dict)``
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_refresh_token_grant(self, request, data, client):
|
||||
"""
|
||||
Return the refresh token associated with this request or an error dict.
|
||||
|
||||
:return: ``tuple`` - ``(True or False, token or error_dict)``
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_password_grant(self, request, data, client):
|
||||
"""
|
||||
Return a user associated with this request or an error dict.
|
||||
|
||||
:return: ``tuple`` - ``(True or False, user or error_dict)``
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_access_token(self, request, user, scope, client):
|
||||
"""
|
||||
Override to handle fetching of an existing access token.
|
||||
|
||||
:return: ``object`` - Access token
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_access_token(self, request, user, scope, client):
|
||||
"""
|
||||
Override to handle access token creation.
|
||||
|
||||
:return: ``object`` - Access token
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_refresh_token(self, request, user, scope, access_token, client):
|
||||
"""
|
||||
Override to handle refresh token creation.
|
||||
|
||||
:return: ``object`` - Refresh token
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def invalidate_grant(self, grant):
|
||||
"""
|
||||
Override to handle grant invalidation. A grant is invalidated right
|
||||
after creating an access token from it.
|
||||
|
||||
:return None:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def invalidate_refresh_token(self, refresh_token):
|
||||
"""
|
||||
Override to handle refresh token invalidation. When requesting a new
|
||||
access token from a refresh token, the old one is *always* invalidated.
|
||||
|
||||
:return None:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def invalidate_access_token(self, access_token):
|
||||
"""
|
||||
Override to handle access token invalidation. When a new access token
|
||||
is created from a refresh token, the old one is *always* invalidated.
|
||||
|
||||
:return None:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def error_response(self, error, mimetype='application/json', status=400,
|
||||
**kwargs):
|
||||
"""
|
||||
Return an error response to the client with default status code of
|
||||
*400* stating the error as outlined in :rfc:`5.2`.
|
||||
"""
|
||||
return HttpResponse(json.dumps(error), content_type=mimetype,
|
||||
status=status, **kwargs)
|
||||
|
||||
def access_token_response(self, access_token):
|
||||
"""
|
||||
Returns a successful response after creating the access token
|
||||
as defined in :rfc:`5.1`.
|
||||
"""
|
||||
|
||||
response_data = {
|
||||
'access_token': access_token.token,
|
||||
'token_type': constants.TOKEN_TYPE,
|
||||
'expires_in': access_token.get_expire_delta(),
|
||||
'scope': ' '.join(scope.names(access_token.scope)),
|
||||
}
|
||||
|
||||
# Not all access_tokens are given a refresh_token
|
||||
# (for example, public clients doing password auth)
|
||||
try:
|
||||
rt = access_token.refresh_token
|
||||
response_data['refresh_token'] = rt.token
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
return HttpResponse(
|
||||
json.dumps(response_data), content_type='application/json'
|
||||
)
|
||||
|
||||
def authorization_code(self, request, data, client):
|
||||
"""
|
||||
Handle ``grant_type=authorization_code`` requests as defined in
|
||||
:rfc:`4.1.3`.
|
||||
"""
|
||||
grant = self.get_authorization_code_grant(request, request.POST,
|
||||
client)
|
||||
if constants.SINGLE_ACCESS_TOKEN:
|
||||
at = self.get_access_token(request, grant.user, grant.scope, client)
|
||||
else:
|
||||
at = self.create_access_token(request, grant.user, grant.scope, client)
|
||||
rt = self.create_refresh_token(request, grant.user, grant.scope, at,
|
||||
client)
|
||||
|
||||
self.invalidate_grant(grant)
|
||||
|
||||
return self.access_token_response(at)
|
||||
|
||||
def refresh_token(self, request, data, client):
|
||||
"""
|
||||
Handle ``grant_type=refresh_token`` requests as defined in :rfc:`6`.
|
||||
"""
|
||||
rt = self.get_refresh_token_grant(request, data, client)
|
||||
|
||||
# this must be called first in case we need to purge expired tokens
|
||||
self.invalidate_refresh_token(rt)
|
||||
self.invalidate_access_token(rt.access_token)
|
||||
|
||||
at = self.create_access_token(request, rt.user, rt.access_token.scope,
|
||||
client)
|
||||
rt = self.create_refresh_token(request, at.user, at.scope, at, client)
|
||||
|
||||
return self.access_token_response(at)
|
||||
|
||||
def password(self, request, data, client):
|
||||
"""
|
||||
Handle ``grant_type=password`` requests as defined in :rfc:`4.3`.
|
||||
"""
|
||||
|
||||
data = self.get_password_grant(request, data, client)
|
||||
user = data.get('user')
|
||||
scope = data.get('scope')
|
||||
|
||||
if constants.SINGLE_ACCESS_TOKEN:
|
||||
at = self.get_access_token(request, user, scope, client)
|
||||
else:
|
||||
at = self.create_access_token(request, user, scope, client)
|
||||
# Public clients don't get refresh tokens
|
||||
if client.client_type != 1:
|
||||
rt = self.create_refresh_token(request, user, scope, at, client)
|
||||
|
||||
return self.access_token_response(at)
|
||||
|
||||
def get_handler(self, grant_type):
|
||||
"""
|
||||
Return a function or method that is capable handling the ``grant_type``
|
||||
requested by the client or return ``None`` to indicate that this type
|
||||
of grant type is not supported, resulting in an error response.
|
||||
"""
|
||||
if grant_type == 'authorization_code':
|
||||
return self.authorization_code
|
||||
elif grant_type == 'refresh_token':
|
||||
return self.refresh_token
|
||||
elif grant_type == 'password':
|
||||
return self.password
|
||||
return None
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
As per :rfc:`3.2` the token endpoint *only* supports POST requests.
|
||||
Returns an error response.
|
||||
"""
|
||||
return self.error_response({
|
||||
'error': 'invalid_request',
|
||||
'error_description': _("Only POST requests allowed.")})
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
As per :rfc:`3.2` the token endpoint *only* supports POST requests.
|
||||
"""
|
||||
if constants.ENFORCE_SECURE and not request.is_secure():
|
||||
return self.error_response({
|
||||
'error': 'invalid_request',
|
||||
'error_description': _("A secure connection is required.")})
|
||||
|
||||
if not 'grant_type' in request.POST:
|
||||
return self.error_response({
|
||||
'error': 'invalid_request',
|
||||
'error_description': _("No 'grant_type' included in the "
|
||||
"request.")})
|
||||
|
||||
grant_type = request.POST['grant_type']
|
||||
|
||||
if grant_type not in self.grant_types:
|
||||
return self.error_response({'error': 'unsupported_grant_type'})
|
||||
|
||||
client = self.authenticate(request)
|
||||
|
||||
if client is None:
|
||||
return self.error_response({'error': 'invalid_client'})
|
||||
|
||||
handler = self.get_handler(grant_type)
|
||||
|
||||
try:
|
||||
return handler(request, request.POST, client)
|
||||
except OAuthError, e:
|
||||
return self.error_response(e.args[0])
|
|
@ -1,2 +0,0 @@
|
|||
Django>=1.4
|
||||
shortuuid>=0.4
|
28
setup.py
28
setup.py
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
import provider
|
||||
|
||||
setup(
|
||||
name='django-oauth2-provider',
|
||||
version=provider.__version__,
|
||||
description='Provide OAuth2 access to your app',
|
||||
long_description=open('README.rst').read(),
|
||||
author='Alen Mujezinovic',
|
||||
author_email='alen@caffeinehit.com',
|
||||
url = 'https://github.com/caffeinehit/django-oauth2-provider',
|
||||
packages= find_packages(exclude=('tests*',)),
|
||||
classifiers=[
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Framework :: Django',
|
||||
],
|
||||
install_requires=[
|
||||
"shortuuid>=0.4"
|
||||
],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
)
|
18
test.sh
18
test.sh
|
@ -1,18 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
DJ_VERSION=$(django-admin.py --version)
|
||||
|
||||
# exit if fail
|
||||
[[ "$?" -ne "0" ]] && exit;
|
||||
|
||||
IS_16=$(echo $DJ_VERSION | grep -E "1\.6|1\.7|1\.8|dev")
|
||||
|
||||
# if django version is not 1.6 (non-0 exit) we have to pass different
|
||||
# app names to test runner
|
||||
if [ "$?" -ne "1" ]; then
|
||||
app_names=( provider provider.oauth2 )
|
||||
else
|
||||
app_names=( provider oauth2 )
|
||||
fi
|
||||
|
||||
python manage.py test ${app_names[@]} --traceback --failfast
|
|
@ -1,76 +0,0 @@
|
|||
# Django settings for example project.
|
||||
import os
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
('Tester', 'test@example.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': '%s/db.sqlite' % os.path.dirname(__file__), # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = ''
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'secret'
|
||||
|
||||
ROOT_URLCONF = 'tests.urls'
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.admin',
|
||||
'provider',
|
||||
'provider.oauth2',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
# Use DiscoverRunner on Django 1.7 and above
|
||||
if DJANGO_VERSION[0] == 1 and DJANGO_VERSION[1] >= 7:
|
||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from provider.compat.urls import *
|
||||
from django.contrib import admin
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')),
|
||||
)
|
45
tox.ini
45
tox.ini
|
@ -1,45 +0,0 @@
|
|||
[tox]
|
||||
downloadcache = {toxworkdir}/cache/
|
||||
envlist = py2.7-django.dev,py2.7-django1.6,py2.7-django1.5,py2.7-django1.4,py2.6-django1.6,py2.6-django1.5,py2.6-django1.4
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}
|
||||
commands =
|
||||
{toxinidir}/test.sh
|
||||
deps =
|
||||
|
||||
[testenv:py2.7-django.dev]
|
||||
basepython = python2.7
|
||||
deps = https://github.com/django/django/zipball/master
|
||||
{[testenv]deps}
|
||||
|
||||
[testenv:py2.7-django1.6]
|
||||
basepython = python2.7
|
||||
deps = django>=1.6,<1.7
|
||||
{[testenv]deps}
|
||||
|
||||
[testenv:py2.7-django1.5]
|
||||
basepython = python2.7
|
||||
deps = django>=1.5,<1.6
|
||||
{[testenv]deps}
|
||||
|
||||
[testenv:py2.7-django1.4]
|
||||
basepython = python2.7
|
||||
deps = django>=1.4,<1.5
|
||||
{[testenv]deps}
|
||||
|
||||
[testenv:py2.6-django1.6]
|
||||
basepython = python2.6
|
||||
deps = django>=1.6,<1.7
|
||||
{[testenv]deps}
|
||||
|
||||
[testenv:py2.6-django1.5]
|
||||
basepython = python2.6
|
||||
deps = django>=1.5,<1.6
|
||||
{[testenv]deps}
|
||||
|
||||
[testenv:py2.6-django1.4]
|
||||
basepython = python2.6
|
||||
deps = django>=1.4,<1.5
|
||||
{[testenv]deps}
|
Loading…
Reference in New Issue