Merge pull request #203 from psavoie/develop
Add pep8 compliance and checker
This commit is contained in:
commit
8149f1f9ab
33 changed files with 365 additions and 231 deletions
142
docs/conf.py
142
docs/conf.py
|
@ -12,18 +12,18 @@
|
|||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
# import sys
|
||||
# import os
|
||||
|
||||
# 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('.'))
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
# 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
|
||||
|
@ -38,7 +38,7 @@ templates_path = ['_templates']
|
|||
# source_suffix = ['.rst']
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
@ -66,9 +66,9 @@ language = None
|
|||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
|
@ -76,27 +76,27 @@ exclude_patterns = ['_build']
|
|||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
# 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
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
# 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 = []
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
@ -111,26 +111,26 @@ html_theme = 'sphinx_rtd_theme'
|
|||
# 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 = {}
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
# 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
|
||||
# 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
|
||||
# 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,
|
||||
|
@ -140,62 +140,62 @@ html_static_path = ['_static']
|
|||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
# html_extra_path = []
|
||||
|
||||
# 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'
|
||||
# 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
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = 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 = ''
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-oidc-providerdoc'
|
||||
|
@ -203,17 +203,17 @@ htmlhelp_basename = 'django-oidc-providerdoc'
|
|||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
# Latex figure (float) alignment
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
|
@ -226,23 +226,23 @@ latex_documents = [
|
|||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
@ -255,7 +255,7 @@ man_pages = [
|
|||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
@ -270,16 +270,16 @@ texinfo_documents = [
|
|||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
|
@ -291,62 +291,62 @@ epub_publisher = author
|
|||
epub_copyright = copyright
|
||||
|
||||
# The basename for the epub file. It defaults to the project name.
|
||||
#epub_basename = project
|
||||
# epub_basename = project
|
||||
|
||||
# The HTML theme for the epub output. Since the default themes are not
|
||||
# optimized for small screen space, using the same theme for HTML and epub
|
||||
# output is usually not wise. This defaults to 'epub', a theme designed to save
|
||||
# visual space.
|
||||
#epub_theme = 'epub'
|
||||
# epub_theme = 'epub'
|
||||
|
||||
# The language of the text. It defaults to the language option
|
||||
# or 'en' if the language is not set.
|
||||
#epub_language = ''
|
||||
# epub_language = ''
|
||||
|
||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||
#epub_scheme = ''
|
||||
# epub_scheme = ''
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#epub_identifier = ''
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#epub_uid = ''
|
||||
# epub_uid = ''
|
||||
|
||||
# A tuple containing the cover image and cover page html template filenames.
|
||||
#epub_cover = ()
|
||||
# epub_cover = ()
|
||||
|
||||
# A sequence of (type, uri, title) tuples for the guide element of content.opf.
|
||||
#epub_guide = ()
|
||||
# epub_guide = ()
|
||||
|
||||
# HTML files that should be inserted before the pages created by sphinx.
|
||||
# The format is a list of tuples containing the path and title.
|
||||
#epub_pre_files = []
|
||||
# epub_pre_files = []
|
||||
|
||||
# HTML files that should be inserted after the pages created by sphinx.
|
||||
# The format is a list of tuples containing the path and title.
|
||||
#epub_post_files = []
|
||||
# epub_post_files = []
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
# The depth of the table of contents in toc.ncx.
|
||||
#epub_tocdepth = 3
|
||||
# epub_tocdepth = 3
|
||||
|
||||
# Allow duplicate toc entries.
|
||||
#epub_tocdup = True
|
||||
# epub_tocdup = True
|
||||
|
||||
# Choose between 'default' and 'includehidden'.
|
||||
#epub_tocscope = 'default'
|
||||
# epub_tocscope = 'default'
|
||||
|
||||
# Fix unsupported image types using the Pillow.
|
||||
#epub_fix_images = False
|
||||
# epub_fix_images = False
|
||||
|
||||
# Scale large images.
|
||||
#epub_max_image_width = 0
|
||||
# epub_max_image_width = 0
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#epub_show_urls = 'inline'
|
||||
# epub_show_urls = 'inline'
|
||||
|
||||
# If false, no index is generated.
|
||||
#epub_use_index = True
|
||||
# epub_use_index = True
|
||||
|
|
|
@ -6,8 +6,8 @@ from django.views.generic import TemplateView
|
|||
|
||||
urlpatterns = [
|
||||
url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'),
|
||||
url(r'^accounts/login/$', auth_views.login, { 'template_name': 'login.html' }, name='login'),
|
||||
url(r'^accounts/logout/$', auth_views.logout, { 'next_page': '/' }, name='logout'),
|
||||
url(r'^accounts/login/$', auth_views.login, {'template_name': 'login.html'}, name='login'),
|
||||
url(r'^accounts/logout/$', auth_views.logout, {'next_page': '/'}, name='logout'),
|
||||
|
||||
url(r'^', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
||||
|
|
|
@ -51,7 +51,9 @@ class ClientAdmin(admin.ModelAdmin):
|
|||
|
||||
fieldsets = [
|
||||
[_(u''), {
|
||||
'fields': ('name', 'client_type', 'response_type','_redirect_uris', 'jwt_alg', 'require_consent', 'reuse_consent'),
|
||||
'fields': (
|
||||
'name', 'client_type', 'response_type', '_redirect_uris', 'jwt_alg', 'require_consent',
|
||||
'reuse_consent'),
|
||||
}],
|
||||
[_(u'Credentials'), {
|
||||
'fields': ('client_id', 'client_secret'),
|
||||
|
|
|
@ -9,8 +9,8 @@ STANDARD_CLAIMS = {
|
|||
'name': '', 'given_name': '', 'family_name': '', 'middle_name': '', 'nickname': '',
|
||||
'preferred_username': '', 'profile': '', 'picture': '', 'website': '', 'gender': '',
|
||||
'birthdate': '', 'zoneinfo': '', 'locale': '', 'updated_at': '', 'email': '', 'email_verified': '',
|
||||
'phone_number': '', 'phone_number_verified': '', 'address': { 'formatted': '',
|
||||
'street_address': '', 'locality': '', 'region': '', 'postal_code': '', 'country': '', },
|
||||
'phone_number': '', 'phone_number_verified': '', 'address': {
|
||||
'formatted': '', 'street_address': '', 'locality': '', 'region': '', 'postal_code': '', 'country': '', },
|
||||
}
|
||||
|
||||
|
||||
|
@ -72,7 +72,9 @@ class ScopeClaims(object):
|
|||
return aux_dic
|
||||
|
||||
@classmethod
|
||||
def get_scopes_info(cls, scopes=[]):
|
||||
def get_scopes_info(cls, scopes=None):
|
||||
if scopes is None:
|
||||
scopes = []
|
||||
scopes_info = []
|
||||
|
||||
for name in cls.__dict__:
|
||||
|
@ -99,6 +101,7 @@ class StandardScopeClaims(ScopeClaims):
|
|||
_(u'Basic profile'),
|
||||
_(u'Access to your basic information. Includes names, gender, birthdate and other information.'),
|
||||
)
|
||||
|
||||
def scope_profile(self):
|
||||
dic = {
|
||||
'name': self.userinfo.get('name'),
|
||||
|
@ -123,6 +126,7 @@ class StandardScopeClaims(ScopeClaims):
|
|||
_(u'Email'),
|
||||
_(u'Access to your email address.'),
|
||||
)
|
||||
|
||||
def scope_email(self):
|
||||
dic = {
|
||||
'email': self.userinfo.get('email') or getattr(self.user, 'email', None),
|
||||
|
@ -135,6 +139,7 @@ class StandardScopeClaims(ScopeClaims):
|
|||
_(u'Phone number'),
|
||||
_(u'Access to your phone number.'),
|
||||
)
|
||||
|
||||
def scope_phone(self):
|
||||
dic = {
|
||||
'phone_number': self.userinfo.get('phone_number'),
|
||||
|
@ -147,6 +152,7 @@ class StandardScopeClaims(ScopeClaims):
|
|||
_(u'Address information'),
|
||||
_(u'Access to your address. Includes country, locality, street and other information.'),
|
||||
)
|
||||
|
||||
def scope_address(self):
|
||||
dic = {
|
||||
'address': {
|
||||
|
|
|
@ -102,8 +102,8 @@ class AuthorizeEndpoint(object):
|
|||
logger.debug('[Authorize] Invalid response type: %s', self.params['response_type'])
|
||||
raise AuthorizeError(self.params['redirect_uri'], 'unsupported_response_type', self.grant_type)
|
||||
|
||||
if not self.is_authentication and \
|
||||
(self.grant_type == 'hybrid' or self.params['response_type'] in ['id_token', 'id_token token']):
|
||||
if (not self.is_authentication and
|
||||
(self.grant_type == 'hybrid' or self.params['response_type'] in ['id_token', 'id_token token'])):
|
||||
logger.debug('[Authorize] Missing openid scope.')
|
||||
raise AuthorizeError(self.params['redirect_uri'], 'invalid_scope', self.grant_type)
|
||||
|
||||
|
@ -165,7 +165,8 @@ class AuthorizeEndpoint(object):
|
|||
id_token_dic = create_id_token(**kwargs)
|
||||
|
||||
# Check if response_type must include id_token in the response.
|
||||
if self.params['response_type'] in ['id_token', 'id_token token', 'code id_token', 'code id_token token']:
|
||||
if self.params['response_type'] in [
|
||||
'id_token', 'id_token token', 'code id_token', 'code id_token token']:
|
||||
query_fragment['id_token'] = encode_id_token(id_token_dic, self.client)
|
||||
else:
|
||||
id_token_dic = {}
|
||||
|
@ -211,7 +212,8 @@ class AuthorizeEndpoint(object):
|
|||
logger.exception('[Authorize] Error when trying to create response uri: %s', error)
|
||||
raise AuthorizeError(self.params['redirect_uri'], 'server_error', self.grant_type)
|
||||
|
||||
uri = uri._replace(query=urlencode(query_params, doseq=True), fragment=uri.fragment + urlencode(query_fragment, doseq=True))
|
||||
uri = uri._replace(
|
||||
query=urlencode(query_params, doseq=True), fragment=uri.fragment + urlencode(query_fragment, doseq=True))
|
||||
|
||||
return urlunsplit(uri)
|
||||
|
||||
|
@ -264,7 +266,8 @@ class AuthorizeEndpoint(object):
|
|||
"""
|
||||
scopes = StandardScopeClaims.get_scopes_info(self.params['scope'])
|
||||
if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'):
|
||||
scopes_extra = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True).get_scopes_info(self.params['scope'])
|
||||
scopes_extra = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True).get_scopes_info(
|
||||
self.params['scope'])
|
||||
for index_extra, scope_extra in enumerate(scopes_extra):
|
||||
for index, scope in enumerate(scopes[:]):
|
||||
if scope_extra['scope'] == scope['scope']:
|
||||
|
|
|
@ -4,11 +4,6 @@ import logging
|
|||
import re
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
try:
|
||||
from urllib.parse import unquote
|
||||
except ImportError:
|
||||
from urllib import unquote
|
||||
|
||||
from django.http import JsonResponse
|
||||
|
||||
from oidc_provider.lib.errors import (
|
||||
|
|
|
@ -31,6 +31,7 @@ class UserAuthError(Exception):
|
|||
'error_description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class AuthorizeError(Exception):
|
||||
|
||||
_errors = {
|
||||
|
|
|
@ -11,11 +11,6 @@ from django.http import HttpResponse
|
|||
|
||||
from oidc_provider import settings
|
||||
|
||||
try:
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
except ImportError:
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
|
||||
def redirect(uri):
|
||||
"""
|
||||
|
@ -81,7 +76,8 @@ def default_after_userlogin_hook(request, user, client):
|
|||
return None
|
||||
|
||||
|
||||
def default_after_end_session_hook(request, id_token=None, post_logout_redirect_uri=None, state=None, client=None, next_page=None):
|
||||
def default_after_end_session_hook(
|
||||
request, id_token=None, post_logout_redirect_uri=None, state=None, client=None, next_page=None):
|
||||
"""
|
||||
Default function for setting OIDC_AFTER_END_SESSION_HOOK.
|
||||
|
||||
|
@ -97,7 +93,8 @@ def default_after_end_session_hook(request, id_token=None, post_logout_redirect_
|
|||
:param state: state param from url query params
|
||||
:type state: str
|
||||
|
||||
:param client: If id_token has `aud` param and associated Client exists, this is an instance of it - do NOT trust this param
|
||||
:param client: If id_token has `aud` param and associated Client exists,
|
||||
this is an instance of it - do NOT trust this param
|
||||
:type client: oidc_provider.models.Client
|
||||
|
||||
:param next_page: calculated next_page redirection target
|
||||
|
|
|
@ -28,12 +28,15 @@ def extract_access_token(request):
|
|||
return access_token
|
||||
|
||||
|
||||
def protected_resource_view(scopes=[]):
|
||||
def protected_resource_view(scopes=None):
|
||||
"""
|
||||
View decorator. The client accesses protected resources by presenting the
|
||||
access token to the resource server.
|
||||
https://tools.ietf.org/html/rfc6749#section-7
|
||||
"""
|
||||
if scopes is None:
|
||||
scopes = []
|
||||
|
||||
def wrapper(view):
|
||||
def view_wrapper(request, *args, **kwargs):
|
||||
access_token = extract_access_token(request)
|
||||
|
@ -52,9 +55,10 @@ def protected_resource_view(scopes=[]):
|
|||
if not set(scopes).issubset(set(kwargs['token'].scope)):
|
||||
logger.debug('[UserInfo] Missing openid scope.')
|
||||
raise BearerTokenError('insufficient_scope')
|
||||
except (BearerTokenError) as error:
|
||||
except BearerTokenError as error:
|
||||
response = HttpResponse(status=error.status)
|
||||
response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(error.code, error.description)
|
||||
response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(
|
||||
error.code, error.description)
|
||||
return response
|
||||
|
||||
return view(request, *args, **kwargs)
|
||||
|
|
|
@ -18,12 +18,14 @@ from oidc_provider.models import (
|
|||
from oidc_provider import settings
|
||||
|
||||
|
||||
def create_id_token(user, aud, nonce='', at_hash='', request=None, scope=[]):
|
||||
def create_id_token(user, aud, nonce='', at_hash='', request=None, scope=None):
|
||||
"""
|
||||
Creates the id_token dictionary.
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
Return a dic.
|
||||
"""
|
||||
if scope is None:
|
||||
scope = []
|
||||
sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR', import_str=True)(user=user)
|
||||
|
||||
expires_in = settings.get('OIDC_IDTOKEN_EXPIRE')
|
||||
|
@ -63,6 +65,7 @@ def create_id_token(user, aud, nonce='', at_hash='', request=None, scope=[]):
|
|||
|
||||
return dic
|
||||
|
||||
|
||||
def encode_id_token(payload, client):
|
||||
"""
|
||||
Represent the ID Token as a JSON Web Token (JWT).
|
||||
|
@ -72,6 +75,7 @@ def encode_id_token(payload, client):
|
|||
_jws = JWS(payload, alg=client.jwt_alg)
|
||||
return _jws.sign_compact(keys)
|
||||
|
||||
|
||||
def decode_id_token(token, client):
|
||||
"""
|
||||
Represent the ID Token as a JSON Web Token (JWT).
|
||||
|
@ -80,6 +84,7 @@ def decode_id_token(token, client):
|
|||
keys = get_client_alg_keys(client)
|
||||
return JWS().verify_compact(token, keys=keys)
|
||||
|
||||
|
||||
def client_id_from_id_token(id_token):
|
||||
"""
|
||||
Extracts the client id from a JSON Web Token (JWT).
|
||||
|
@ -88,6 +93,7 @@ def client_id_from_id_token(id_token):
|
|||
payload = JWT().unpack(id_token).payload()
|
||||
return payload.get('aud', None)
|
||||
|
||||
|
||||
def create_token(user, client, scope, id_token_dic=None):
|
||||
"""
|
||||
Create and populate a Token object.
|
||||
|
@ -108,6 +114,7 @@ def create_token(user, client, scope, id_token_dic=None):
|
|||
|
||||
return token
|
||||
|
||||
|
||||
def create_code(user, client, scope, nonce, is_authentication,
|
||||
code_challenge=None, code_challenge_method=None):
|
||||
"""
|
||||
|
@ -132,6 +139,7 @@ def create_code(user, client, scope, nonce, is_authentication,
|
|||
|
||||
return code
|
||||
|
||||
|
||||
def get_client_alg_keys(client):
|
||||
"""
|
||||
Takes a client and returns the set of keys associated with it.
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import os
|
||||
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from oidc_provider import settings
|
||||
from oidc_provider.models import RSAKey
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ class Migration(migrations.Migration):
|
|||
('name', models.CharField(default=b'', max_length=100)),
|
||||
('client_id', models.CharField(unique=True, max_length=255)),
|
||||
('client_secret', models.CharField(unique=True, max_length=255)),
|
||||
('response_type', models.CharField(max_length=30, choices=[(b'code', b'code (Authorization Code Flow)'), (b'id_token', b'id_token (Implicit Flow)'), (b'id_token token', b'id_token token (Implicit Flow)')])),
|
||||
('response_type', models.CharField(max_length=30, choices=[
|
||||
(b'code', b'code (Authorization Code Flow)'), (b'id_token', b'id_token (Implicit Flow)'),
|
||||
(b'id_token token', b'id_token token (Implicit Flow)')])),
|
||||
('_redirect_uris', models.TextField(default=b'')),
|
||||
],
|
||||
options={
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -29,7 +29,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='date_created',
|
||||
field=models.DateField(auto_now_add=True, default=datetime.datetime(2016, 1, 11, 18, 44, 32, 192477, tzinfo=utc)),
|
||||
field=models.DateField(
|
||||
auto_now_add=True, default=datetime.datetime(2016, 1, 11, 18, 44, 32, 192477, tzinfo=utc)),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
|
|
@ -15,6 +15,11 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='client_type',
|
||||
field=models.CharField(choices=[(b'confidential', b'Confidential'), (b'public', b'Public')], default=b'confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30),
|
||||
field=models.CharField(
|
||||
choices=[(b'confidential', b'Confidential'), (b'public', b'Public')],
|
||||
default=b'confidential',
|
||||
help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their '
|
||||
'credentials. <b>Public</b> clients are incapable.',
|
||||
max_length=30),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -15,6 +15,10 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='jwt_alg',
|
||||
field=models.CharField(choices=[(b'HS256', b'HS256'), (b'RS256', b'RS256')], default=b'RS256', max_length=10, verbose_name='JWT Algorithm'),
|
||||
field=models.CharField(
|
||||
choices=[(b'HS256', b'HS256'), (b'RS256', b'RS256')],
|
||||
default=b'RS256',
|
||||
max_length=10,
|
||||
verbose_name='JWT Algorithm'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -25,12 +25,21 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='client_type',
|
||||
field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], default='confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30),
|
||||
field=models.CharField(
|
||||
choices=[('confidential', 'Confidential'), ('public', 'Public')],
|
||||
default='confidential',
|
||||
help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their'
|
||||
' credentials. <b>Public</b> clients are incapable.',
|
||||
max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='jwt_alg',
|
||||
field=models.CharField(choices=[('HS256', 'HS256'), ('RS256', 'RS256')], default='RS256', max_length=10, verbose_name='JWT Algorithm'),
|
||||
field=models.CharField(
|
||||
choices=[('HS256', 'HS256'), ('RS256', 'RS256')],
|
||||
default='RS256',
|
||||
max_length=10,
|
||||
verbose_name='JWT Algorithm'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
|
@ -40,7 +49,11 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='response_type',
|
||||
field=models.CharField(choices=[('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)')], max_length=30),
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'),
|
||||
('id_token token', 'id_token token (Implicit Flow)')],
|
||||
max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='code',
|
||||
|
|
|
@ -19,13 +19,15 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='userconsent',
|
||||
name='date_given',
|
||||
field=models.DateTimeField(default=datetime.datetime(2016, 6, 10, 17, 53, 48, 889808, tzinfo=utc), verbose_name='Date Given'),
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime(2016, 6, 10, 17, 53, 48, 889808, tzinfo=utc), verbose_name='Date Given'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='_redirect_uris',
|
||||
field=models.TextField(default=b'', help_text='Enter each URI on a new line.', verbose_name='Redirect URIs'),
|
||||
field=models.TextField(
|
||||
default=b'', help_text='Enter each URI on a new line.', verbose_name='Redirect URIs'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
|
@ -40,7 +42,13 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='client_type',
|
||||
field=models.CharField(choices=[(b'confidential', b'Confidential'), (b'public', b'Public')], default=b'confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30, verbose_name='Client Type'),
|
||||
field=models.CharField(
|
||||
choices=[(b'confidential', b'Confidential'), (b'public', b'Public')],
|
||||
default=b'confidential',
|
||||
help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their '
|
||||
'credentials. <b>Public</b> clients are incapable.',
|
||||
max_length=30,
|
||||
verbose_name='Client Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
|
@ -55,7 +63,12 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='response_type',
|
||||
field=models.CharField(choices=[(b'code', b'code (Authorization Code Flow)'), (b'id_token', b'id_token (Implicit Flow)'), (b'id_token token', b'id_token token (Implicit Flow)')], max_length=30, verbose_name='Response Type'),
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
(b'code', b'code (Authorization Code Flow)'), (b'id_token', b'id_token (Implicit Flow)'),
|
||||
(b'id_token token', b'id_token token (Implicit Flow)')],
|
||||
max_length=30,
|
||||
verbose_name='Response Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='code',
|
||||
|
@ -65,7 +78,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='code',
|
||||
name='client',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='code',
|
||||
|
@ -100,7 +114,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='code',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rsakey',
|
||||
|
@ -125,7 +140,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='token',
|
||||
name='client',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='token',
|
||||
|
@ -140,7 +156,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='token',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userconsent',
|
||||
|
@ -150,7 +167,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='userconsent',
|
||||
name='client',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userconsent',
|
||||
|
@ -160,6 +178,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='userconsent',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -25,7 +25,13 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='client_type',
|
||||
field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], default='confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30, verbose_name='Client Type'),
|
||||
field=models.CharField(
|
||||
choices=[('confidential', 'Confidential'), ('public', 'Public')],
|
||||
default='confidential',
|
||||
help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their '
|
||||
'credentials. <b>Public</b> clients are incapable.',
|
||||
max_length=30,
|
||||
verbose_name='Client Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
|
@ -35,7 +41,12 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='response_type',
|
||||
field=models.CharField(choices=[('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)')], max_length=30, verbose_name='Response Type'),
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'),
|
||||
('id_token token', 'id_token token (Implicit Flow)')],
|
||||
max_length=30,
|
||||
verbose_name='Response Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='code',
|
||||
|
|
|
@ -20,12 +20,18 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='logo',
|
||||
field=models.FileField(blank=True, default='', upload_to='oidc_provider/clients', verbose_name='Logo Image'),
|
||||
field=models.FileField(
|
||||
blank=True, default='', upload_to='oidc_provider/clients', verbose_name='Logo Image'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='terms_url',
|
||||
field=models.CharField(blank=True, default='', help_text='External reference to the privacy policy of the client.', max_length=255, verbose_name='Terms URL'),
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='External reference to the privacy policy of the client.',
|
||||
max_length=255,
|
||||
verbose_name='Terms URL'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='client',
|
||||
|
@ -35,11 +41,23 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='jwt_alg',
|
||||
field=models.CharField(choices=[('HS256', 'HS256'), ('RS256', 'RS256')], default='RS256', help_text='Algorithm used to encode ID Tokens.', max_length=10, verbose_name='JWT Algorithm'),
|
||||
field=models.CharField(
|
||||
choices=[('HS256', 'HS256'), ('RS256', 'RS256')],
|
||||
default='RS256',
|
||||
help_text='Algorithm used to encode ID Tokens.',
|
||||
max_length=10,
|
||||
verbose_name='JWT Algorithm'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='response_type',
|
||||
field=models.CharField(choices=[('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)'), ('code token', 'code token (Hybrid Flow)'), ('code id_token', 'code id_token (Hybrid Flow)'), ('code id_token token', 'code id_token token (Hybrid Flow)')], max_length=30, verbose_name='Response Type'),
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'),
|
||||
('id_token token', 'id_token token (Implicit Flow)'), ('code token', 'code token (Hybrid Flow)'),
|
||||
('code id_token', 'code id_token (Hybrid Flow)'),
|
||||
('code id_token token', 'code id_token token (Hybrid Flow)')],
|
||||
max_length=30,
|
||||
verbose_name='Response Type'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -15,6 +15,10 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='_post_logout_redirect_uris',
|
||||
field=models.TextField(blank=True, default='', help_text='Enter each URI on a new line.', verbose_name='Post Logout Redirect URIs'),
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='Enter each URI on a new line.',
|
||||
verbose_name='Post Logout Redirect URIs'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -15,11 +15,18 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='require_consent',
|
||||
field=models.BooleanField(default=True, help_text='If disabled, the Server will NEVER ask the user for consent.', verbose_name='Require Consent?'),
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text='If disabled, the Server will NEVER ask the user for consent.',
|
||||
verbose_name='Require Consent?'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='reuse_consent',
|
||||
field=models.BooleanField(default=True, help_text="If enabled, the Server will save the user consent given to a specific client, so that user won't be prompted for the same authorization multiple times.", verbose_name='Reuse Consent?'),
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="If enabled, the Server will save the user consent given to a specific client,"
|
||||
" so that user won't be prompted for the same authorization multiple times.",
|
||||
verbose_name='Reuse Consent?'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -33,36 +33,66 @@ JWT_ALGS = [
|
|||
class Client(models.Model):
|
||||
|
||||
name = models.CharField(max_length=100, default='', verbose_name=_(u'Name'))
|
||||
client_type = models.CharField(max_length=30, choices=CLIENT_TYPE_CHOICES, default='confidential', verbose_name=_(u'Client Type'), help_text=_(u'<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.'))
|
||||
client_type = models.CharField(
|
||||
max_length=30,
|
||||
choices=CLIENT_TYPE_CHOICES,
|
||||
default='confidential',
|
||||
verbose_name=_(u'Client Type'),
|
||||
help_text=_(u'<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. '
|
||||
u'<b>Public</b> clients are incapable.'))
|
||||
client_id = models.CharField(max_length=255, unique=True, verbose_name=_(u'Client ID'))
|
||||
client_secret = models.CharField(max_length=255, blank=True, verbose_name=_(u'Client SECRET'))
|
||||
response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES, verbose_name=_(u'Response Type'))
|
||||
jwt_alg = models.CharField(max_length=10, choices=JWT_ALGS, default='RS256', verbose_name=_(u'JWT Algorithm'), help_text=_(u'Algorithm used to encode ID Tokens.'))
|
||||
jwt_alg = models.CharField(
|
||||
max_length=10,
|
||||
choices=JWT_ALGS,
|
||||
default='RS256',
|
||||
verbose_name=_(u'JWT Algorithm'),
|
||||
help_text=_(u'Algorithm used to encode ID Tokens.'))
|
||||
date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created'))
|
||||
website_url = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Website URL'))
|
||||
terms_url = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Terms URL'), help_text=_(u'External reference to the privacy policy of the client.'))
|
||||
terms_url = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default='',
|
||||
verbose_name=_(u'Terms URL'),
|
||||
help_text=_(u'External reference to the privacy policy of the client.'))
|
||||
contact_email = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Contact Email'))
|
||||
logo = models.FileField(blank=True, default='', upload_to='oidc_provider/clients', verbose_name=_(u'Logo Image'))
|
||||
reuse_consent = models.BooleanField(default=True, verbose_name=_('Reuse Consent?'), help_text=_('If enabled, the Server will save the user consent given to a specific client, so that user won\'t be prompted for the same authorization multiple times.'))
|
||||
require_consent = models.BooleanField(default=True, verbose_name=_('Require Consent?'), help_text=_('If disabled, the Server will NEVER ask the user for consent.'))
|
||||
reuse_consent = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_('Reuse Consent?'),
|
||||
help_text=_('If enabled, the Server will save the user consent given to a specific client, so that'
|
||||
' user won\'t be prompted for the same authorization multiple times.'))
|
||||
require_consent = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_('Require Consent?'),
|
||||
help_text=_('If disabled, the Server will NEVER ask the user for consent.'))
|
||||
|
||||
_redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.'))
|
||||
def redirect_uris():
|
||||
def fget(self):
|
||||
return self._redirect_uris.splitlines()
|
||||
def fset(self, value):
|
||||
self._redirect_uris = '\n'.join(value)
|
||||
return locals()
|
||||
redirect_uris = property(**redirect_uris())
|
||||
_redirect_uris = models.TextField(
|
||||
default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.'))
|
||||
|
||||
_post_logout_redirect_uris = models.TextField(blank=True, default='', verbose_name=_(u'Post Logout Redirect URIs'), help_text=_(u'Enter each URI on a new line.'))
|
||||
def post_logout_redirect_uris():
|
||||
def fget(self):
|
||||
return self._post_logout_redirect_uris.splitlines()
|
||||
def fset(self, value):
|
||||
self._post_logout_redirect_uris = '\n'.join(value)
|
||||
return locals()
|
||||
post_logout_redirect_uris = property(**post_logout_redirect_uris())
|
||||
@property
|
||||
def redirect_uris(self):
|
||||
return self._redirect_uris.splitlines()
|
||||
|
||||
@redirect_uris.setter
|
||||
def redirect_uris(self, value):
|
||||
self._redirect_uris = '\n'.join(value)
|
||||
|
||||
_post_logout_redirect_uris = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
verbose_name=_(u'Post Logout Redirect URIs'),
|
||||
help_text=_(u'Enter each URI on a new line.'))
|
||||
|
||||
@property
|
||||
def post_logout_redirect_uris(self):
|
||||
return self._post_logout_redirect_uris.splitlines()
|
||||
|
||||
@post_logout_redirect_uris.setter
|
||||
def post_logout_redirect_uris(self, value):
|
||||
self._post_logout_redirect_uris = '\n'.join(value)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _(u'Client')
|
||||
|
@ -74,8 +104,6 @@ class Client(models.Model):
|
|||
def __unicode__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def default_redirect_uri(self):
|
||||
return self.redirect_uris[0] if self.redirect_uris else ''
|
||||
|
@ -88,15 +116,13 @@ class BaseCodeTokenModel(models.Model):
|
|||
expires_at = models.DateTimeField(verbose_name=_(u'Expiration Date'))
|
||||
_scope = models.TextField(default='', verbose_name=_(u'Scopes'))
|
||||
|
||||
def scope():
|
||||
def fget(self):
|
||||
return self._scope.split()
|
||||
@property
|
||||
def scope(self):
|
||||
return self._scope.split()
|
||||
|
||||
def fset(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
|
||||
return locals()
|
||||
scope = property(**scope())
|
||||
@scope.setter
|
||||
def scope(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
|
||||
def has_expired(self):
|
||||
return timezone.now() >= self.expires_at
|
||||
|
@ -130,16 +156,13 @@ class Token(BaseCodeTokenModel):
|
|||
refresh_token = models.CharField(max_length=255, unique=True, verbose_name=_(u'Refresh Token'))
|
||||
_id_token = models.TextField(verbose_name=_(u'ID Token'))
|
||||
|
||||
def id_token():
|
||||
@property
|
||||
def id_token(self):
|
||||
return json.loads(self._id_token)
|
||||
|
||||
def fget(self):
|
||||
return json.loads(self._id_token)
|
||||
|
||||
def fset(self, value):
|
||||
self._id_token = json.dumps(value)
|
||||
|
||||
return locals()
|
||||
id_token = property(**id_token())
|
||||
@id_token.setter
|
||||
def id_token(self, value):
|
||||
self._id_token = json.dumps(value)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _(u'Token')
|
||||
|
|
|
@ -145,6 +145,7 @@ class DefaultSettings(object):
|
|||
'error': 'oidc_provider/error.html'
|
||||
}
|
||||
|
||||
|
||||
default_settings = DefaultSettings()
|
||||
|
||||
|
||||
|
|
|
@ -34,7 +34,9 @@ from oidc_provider.lib.endpoints.authorize import AuthorizeEndpoint
|
|||
|
||||
class AuthorizeEndpointMixin(object):
|
||||
|
||||
def _auth_request(self, method, data={}, is_user_authenticated=False):
|
||||
def _auth_request(self, method, data=None, is_user_authenticated=False):
|
||||
if data is None:
|
||||
data = {}
|
||||
url = reverse('oidc_provider:authorize')
|
||||
|
||||
if method.lower() == 'get':
|
||||
|
@ -67,7 +69,8 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
self.client = create_fake_client(response_type='code')
|
||||
self.client_with_no_consent = create_fake_client(response_type='code', require_consent=False)
|
||||
self.client_public = create_fake_client(response_type='code', is_public=True)
|
||||
self.client_public_with_no_consent = create_fake_client(response_type='code', is_public=True, require_consent=False)
|
||||
self.client_public_with_no_consent = create_fake_client(
|
||||
response_type='code', is_public=True, require_consent=False)
|
||||
self.state = uuid.uuid4().hex
|
||||
self.nonce = uuid.uuid4().hex
|
||||
|
||||
|
@ -163,8 +166,7 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
|
||||
for key, value in iter(to_check.items()):
|
||||
is_input_ok = input_html.format(key, value) in response.content.decode('utf-8')
|
||||
self.assertEqual(is_input_ok, True,
|
||||
msg='Hidden input for "' + key + '" fails.')
|
||||
self.assertEqual(is_input_ok, True, msg='Hidden input for "' + key + '" fails.')
|
||||
|
||||
def test_user_consent_response(self):
|
||||
"""
|
||||
|
@ -204,8 +206,7 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
is_code_ok = is_code_valid(url=response['Location'],
|
||||
user=self.user,
|
||||
client=self.client)
|
||||
self.assertEqual(is_code_ok, True,
|
||||
msg='Code returned is invalid.')
|
||||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
||||
|
||||
# Check if the state is returned.
|
||||
state = (response['Location'].split('state='))[1].split('&')[0]
|
||||
|
@ -276,9 +277,10 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
client=self.client)
|
||||
self.assertTrue(is_code_ok, msg='Code returned is invalid or missing')
|
||||
|
||||
self.assertEquals(set(params.keys()), set(['state', 'code']), msg='More than state or code appended as query params')
|
||||
self.assertEquals(set(params.keys()), {'state', 'code'}, msg='More than state or code appended as query params')
|
||||
|
||||
self.assertTrue(response['Location'].startswith(self.client.default_redirect_uri), msg='Different redirect_uri returned')
|
||||
self.assertTrue(
|
||||
response['Location'].startswith(self.client.default_redirect_uri), msg='Different redirect_uri returned')
|
||||
|
||||
def test_unknown_redirect_uris_are_rejected(self):
|
||||
"""
|
||||
|
@ -372,7 +374,8 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
self.assertNotIn(
|
||||
quote('prompt=login'),
|
||||
response['Location'],
|
||||
"Found prompt=login, this leads to infinite login loop. See https://github.com/juanifioren/django-oidc-provider/issues/197."
|
||||
"Found prompt=login, this leads to infinite login loop. See "
|
||||
"https://github.com/juanifioren/django-oidc-provider/issues/197."
|
||||
)
|
||||
|
||||
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||
|
@ -381,7 +384,8 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
self.assertNotIn(
|
||||
quote('prompt=login'),
|
||||
response['Location'],
|
||||
"Found prompt=login, this leads to infinite login loop. See https://github.com/juanifioren/django-oidc-provider/issues/197."
|
||||
"Found prompt=login, this leads to infinite login loop. See "
|
||||
"https://github.com/juanifioren/django-oidc-provider/issues/197."
|
||||
)
|
||||
|
||||
def test_prompt_login_none_parameter(self):
|
||||
|
@ -447,7 +451,6 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|||
self.assertIn('consent_required', response['Location'])
|
||||
|
||||
|
||||
|
||||
class AuthorizationImplicitFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
||||
"""
|
||||
Test cases for Authorization Endpoint using Implicit Flow.
|
||||
|
|
|
@ -50,5 +50,6 @@ class EndSessionTestCase(TestCase):
|
|||
def test_call_post_end_session_hook(self, hook_function):
|
||||
self.client.get(self.url)
|
||||
self.assertTrue(hook_function.called, 'OIDC_AFTER_END_SESSION_HOOK should be called')
|
||||
self.assertTrue(hook_function.call_count == 1, 'OIDC_AFTER_END_SESSION_HOOK should be called once but was {}'.format(hook_function.call_count))
|
||||
|
||||
self.assertTrue(
|
||||
hook_function.call_count == 1,
|
||||
'OIDC_AFTER_END_SESSION_HOOK should be called once but was {}'.format(hook_function.call_count))
|
||||
|
|
|
@ -10,6 +10,7 @@ class StubbedViews:
|
|||
|
||||
urlpatterns = [url('^test/', SampleView.as_view())]
|
||||
|
||||
|
||||
MW_CLASSES = ('django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'oidc_provider.middleware.SessionManagementMiddleware')
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from django.test import TestCase
|
|||
from jwkest.jwk import KEYS
|
||||
from jwkest.jws import JWS
|
||||
from jwkest.jwt import JWT
|
||||
from mock import patch, Mock
|
||||
from mock import patch
|
||||
|
||||
from oidc_provider.lib.utils.token import create_code
|
||||
from oidc_provider.models import Token
|
||||
|
@ -101,7 +101,8 @@ class TokenTestCase(TestCase):
|
|||
"""
|
||||
url = reverse('oidc_provider:token')
|
||||
|
||||
request = self.factory.post(url,
|
||||
request = self.factory.post(
|
||||
url,
|
||||
data=urlencode(post_data),
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
**extras)
|
||||
|
@ -371,7 +372,7 @@ class TokenTestCase(TestCase):
|
|||
|
||||
response_dic2 = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
if scope and set(scope) - set(code.scope): # too broad scope
|
||||
if scope and set(scope) - set(code.scope): # too broad scope
|
||||
self.assertEqual(response.status_code, 400) # Bad Request
|
||||
self.assertIn('error', response_dic2)
|
||||
self.assertEqual(response_dic2['error'], 'invalid_scope')
|
||||
|
@ -427,7 +428,6 @@ class TokenTestCase(TestCase):
|
|||
See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest and
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#HybridTokenRequest.
|
||||
"""
|
||||
SIGKEYS = self._get_keys()
|
||||
code = self._create_code()
|
||||
post_data = self._auth_code_post_data(code=code.code)
|
||||
|
||||
|
@ -465,15 +465,13 @@ class TokenTestCase(TestCase):
|
|||
for request in requests:
|
||||
response = TokenView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code == 405, True,
|
||||
msg=request.method + ' request does not return a 405 status.')
|
||||
self.assertEqual(response.status_code, 405, msg=request.method + ' request does not return a 405 status.')
|
||||
|
||||
request = self.factory.post(url)
|
||||
|
||||
response = TokenView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code == 400, True,
|
||||
msg=request.method + ' request does not return a 400 status.')
|
||||
self.assertEqual(response.status_code, 400, msg=request.method + ' request does not return a 400 status.')
|
||||
|
||||
def test_client_authentication(self):
|
||||
"""
|
||||
|
@ -490,9 +488,10 @@ class TokenTestCase(TestCase):
|
|||
|
||||
response = self._post_request(post_data)
|
||||
|
||||
self.assertEqual('invalid_client' in response.content.decode('utf-8'),
|
||||
False,
|
||||
msg='Client authentication fails using request-body credentials.')
|
||||
self.assertNotIn(
|
||||
'invalid_client',
|
||||
response.content.decode('utf-8'),
|
||||
msg='Client authentication fails using request-body credentials.')
|
||||
|
||||
# Now, test with an invalid client_id.
|
||||
invalid_data = post_data.copy()
|
||||
|
@ -504,9 +503,10 @@ class TokenTestCase(TestCase):
|
|||
|
||||
response = self._post_request(invalid_data)
|
||||
|
||||
self.assertEqual('invalid_client' in response.content.decode('utf-8'),
|
||||
True,
|
||||
msg='Client authentication success with an invalid "client_id".')
|
||||
self.assertIn(
|
||||
'invalid_client',
|
||||
response.content.decode('utf-8'),
|
||||
msg='Client authentication success with an invalid "client_id".')
|
||||
|
||||
# Now, test using HTTP Basic Authentication method.
|
||||
basicauth_data = post_data.copy()
|
||||
|
@ -521,9 +521,10 @@ class TokenTestCase(TestCase):
|
|||
response = self._post_request(basicauth_data, self._password_grant_auth_header())
|
||||
response.content.decode('utf-8')
|
||||
|
||||
self.assertEqual('invalid_client' in response.content.decode('utf-8'),
|
||||
False,
|
||||
msg='Client authentication fails using HTTP Basic Auth.')
|
||||
self.assertNotIn(
|
||||
'invalid_client',
|
||||
response.content.decode('utf-8'),
|
||||
msg='Client authentication fails using HTTP Basic Auth.')
|
||||
|
||||
def test_access_token_contains_nonce(self):
|
||||
"""
|
||||
|
@ -588,7 +589,7 @@ class TokenTestCase(TestCase):
|
|||
response = self._post_request(post_data)
|
||||
response_dic = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
id_token = JWS().verify_compact(response_dic['id_token'].encode('utf-8'), RSAKEYS)
|
||||
JWS().verify_compact(response_dic['id_token'].encode('utf-8'), RSAKEYS)
|
||||
|
||||
@override_settings(OIDC_IDTOKEN_SUB_GENERATOR='oidc_provider.tests.app.utils.fake_sub_generator')
|
||||
def test_custom_sub_generator(self):
|
||||
|
@ -732,4 +733,4 @@ class TokenTestCase(TestCase):
|
|||
|
||||
response = self._post_request(post_data)
|
||||
|
||||
response_dic = json.loads(response.content.decode('utf-8'))
|
||||
json.loads(response.content.decode('utf-8'))
|
||||
|
|
|
@ -30,10 +30,12 @@ class UserInfoTestCase(TestCase):
|
|||
self.user = create_fake_user()
|
||||
self.client = create_fake_client(response_type='code')
|
||||
|
||||
def _create_token(self, extra_scope=[]):
|
||||
def _create_token(self, extra_scope=None):
|
||||
"""
|
||||
Generate a valid token.
|
||||
"""
|
||||
if extra_scope is None:
|
||||
extra_scope = []
|
||||
scope = ['openid', 'email'] + extra_scope
|
||||
|
||||
id_token_dic = create_id_token(
|
||||
|
@ -60,9 +62,7 @@ class UserInfoTestCase(TestCase):
|
|||
"""
|
||||
url = reverse('oidc_provider:userinfo')
|
||||
|
||||
request = self.factory.post(url,
|
||||
data={},
|
||||
content_type='multipart/form-data')
|
||||
request = self.factory.post(url, data={}, content_type='multipart/form-data')
|
||||
|
||||
request.META['HTTP_AUTHORIZATION'] = 'Bearer ' + access_token
|
||||
|
||||
|
@ -136,17 +136,13 @@ class UserInfoTestCase(TestCase):
|
|||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(bool(response.content), True)
|
||||
self.assertEqual('given_name' in response_dic, True,
|
||||
msg='"given_name" claim should be in response.')
|
||||
self.assertEqual('profile' in response_dic, False,
|
||||
msg='"profile" claim should not be in response.')
|
||||
self.assertIn('given_name', response_dic, msg='"given_name" claim should be in response.')
|
||||
self.assertNotIn('profile', response_dic, msg='"profile" claim should not be in response.')
|
||||
|
||||
# Now adding `address` scope.
|
||||
token = self._create_token(extra_scope=['profile', 'address'])
|
||||
response = self._post_request(token.access_token)
|
||||
response_dic = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
self.assertEqual('address' in response_dic, True,
|
||||
msg='"address" claim should be in response.')
|
||||
self.assertEqual('country' in response_dic['address'], True,
|
||||
msg='"country" claim should be in response.')
|
||||
self.assertIn('address', response_dic, msg='"address" claim should be in response.')
|
||||
self.assertIn('country', response_dic['address'], msg='"country" claim should be in response.')
|
||||
|
|
|
@ -84,7 +84,8 @@ class AuthorizeView(View):
|
|||
if 'select_account' in authorize.params['prompt']:
|
||||
# TODO: see how we can support multiple accounts for the end-user.
|
||||
if 'none' in authorize.params['prompt']:
|
||||
raise AuthorizeError(authorize.params['redirect_uri'], 'account_selection_required', authorize.grant_type)
|
||||
raise AuthorizeError(
|
||||
authorize.params['redirect_uri'], 'account_selection_required', authorize.grant_type)
|
||||
else:
|
||||
django_user_logout(request)
|
||||
return redirect_to_login(request.get_full_path(), settings.get('OIDC_LOGIN_URL'))
|
||||
|
@ -92,7 +93,7 @@ class AuthorizeView(View):
|
|||
if {'none', 'consent'}.issubset(authorize.params['prompt']):
|
||||
raise AuthorizeError(authorize.params['redirect_uri'], 'consent_required', authorize.grant_type)
|
||||
|
||||
implicit_flow_resp_types = set(['id_token', 'id_token token'])
|
||||
implicit_flow_resp_types = {'id_token', 'id_token token'}
|
||||
allow_skipping_consent = (
|
||||
authorize.client.client_type != 'public' or
|
||||
authorize.client.response_type in implicit_flow_resp_types)
|
||||
|
@ -162,13 +163,15 @@ class AuthorizeView(View):
|
|||
authorize.validate_params()
|
||||
|
||||
if not request.POST.get('allow'):
|
||||
signals.user_decline_consent.send(self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope'])
|
||||
signals.user_decline_consent.send(
|
||||
self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope'])
|
||||
|
||||
raise AuthorizeError(authorize.params['redirect_uri'],
|
||||
'access_denied',
|
||||
authorize.grant_type)
|
||||
|
||||
signals.user_accept_consent.send(self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope'])
|
||||
signals.user_accept_consent.send(
|
||||
self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope'])
|
||||
|
||||
# Save the user consent given to the client.
|
||||
authorize.set_client_user_consent()
|
||||
|
@ -177,7 +180,7 @@ class AuthorizeView(View):
|
|||
|
||||
return redirect(uri)
|
||||
|
||||
except (AuthorizeError) as error:
|
||||
except AuthorizeError as error:
|
||||
uri = error.create_uri(
|
||||
authorize.params['redirect_uri'],
|
||||
authorize.params['state'])
|
||||
|
|
26
runtests.py
26
runtests.py
|
@ -9,24 +9,24 @@ from django.conf import settings
|
|||
|
||||
DEFAULT_SETTINGS = dict(
|
||||
|
||||
DEBUG = False,
|
||||
DEBUG=False,
|
||||
|
||||
DATABASES = {
|
||||
DATABASES={
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
}
|
||||
},
|
||||
|
||||
SITE_ID = 1,
|
||||
SITE_ID=1,
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
MIDDLEWARE_CLASSES=[
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
],
|
||||
|
||||
TEMPLATES = [
|
||||
TEMPLATES=[
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
|
@ -42,7 +42,7 @@ DEFAULT_SETTINGS = dict(
|
|||
},
|
||||
],
|
||||
|
||||
LOGGING = {
|
||||
LOGGING={
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
|
@ -58,7 +58,7 @@ DEFAULT_SETTINGS = dict(
|
|||
},
|
||||
},
|
||||
|
||||
INSTALLED_APPS = [
|
||||
INSTALLED_APPS=[
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
|
@ -68,20 +68,20 @@ DEFAULT_SETTINGS = dict(
|
|||
'oidc_provider',
|
||||
],
|
||||
|
||||
SECRET_KEY = 'this-should-be-top-secret',
|
||||
SECRET_KEY='this-should-be-top-secret',
|
||||
|
||||
ROOT_URLCONF = 'oidc_provider.tests.app.urls',
|
||||
ROOT_URLCONF='oidc_provider.tests.app.urls',
|
||||
|
||||
TEMPLATE_DIRS = [
|
||||
TEMPLATE_DIRS=[
|
||||
'oidc_provider/tests/templates',
|
||||
],
|
||||
|
||||
USE_TZ = True,
|
||||
USE_TZ=True,
|
||||
|
||||
# OIDC Provider settings.
|
||||
|
||||
SITE_URL = 'http://localhost:8000',
|
||||
OIDC_USERINFO = 'oidc_provider.tests.app.utils.userinfo',
|
||||
SITE_URL='http://localhost:8000',
|
||||
OIDC_USERINFO='oidc_provider.tests.app.utils.userinfo',
|
||||
|
||||
)
|
||||
|
||||
|
|
7
tox.ini
7
tox.ini
|
@ -6,6 +6,7 @@ envlist=
|
|||
py34-django{17,18,19,110,111},
|
||||
py35-django{18,19,110,111},
|
||||
py36-django{18,19,110,111},
|
||||
flake8
|
||||
|
||||
[testenv]
|
||||
|
||||
|
@ -30,3 +31,9 @@ commands=
|
|||
|
||||
commands=
|
||||
coverage report -m
|
||||
|
||||
[testenv:flake8]
|
||||
basepython=python
|
||||
deps=flake8
|
||||
commands =
|
||||
flake8 --max-line-length=120
|
||||
|
|
Loading…
Reference in a new issue