diff --git a/docs/conf.py b/docs/conf.py index e3760f6..56ebcdb 100644 --- a/docs/conf.py +++ b/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 = 'alabaster' # 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 # " v 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 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 diff --git a/example_project/myapp/urls.py b/example_project/myapp/urls.py index 91d31fa..aac3450 100644 --- a/example_project/myapp/urls.py +++ b/example_project/myapp/urls.py @@ -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')), diff --git a/example_project/myapp/wsgi.py b/example_project/myapp/wsgi.py index 91caa07..dd74e93 100644 --- a/example_project/myapp/wsgi.py +++ b/example_project/myapp/wsgi.py @@ -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() diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py index 5ff343b..9197849 100644 --- a/oidc_provider/admin.py +++ b/oidc_provider/admin.py @@ -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'), diff --git a/oidc_provider/lib/claims.py b/oidc_provider/lib/claims.py index fe2e716..d4af2ad 100644 --- a/oidc_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -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': { diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 463a35c..3313bd6 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -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']: diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index abba70a..5a6b0af 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -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 ( diff --git a/oidc_provider/lib/errors.py b/oidc_provider/lib/errors.py index 47f4b10..8533a75 100644 --- a/oidc_provider/lib/errors.py +++ b/oidc_provider/lib/errors.py @@ -31,6 +31,7 @@ class UserAuthError(Exception): 'error_description': self.description, } + class AuthorizeError(Exception): _errors = { diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 09bdf00..c69a1b8 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -5,11 +5,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): """ @@ -75,7 +70,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. @@ -91,7 +87,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 diff --git a/oidc_provider/lib/utils/oauth2.py b/oidc_provider/lib/utils/oauth2.py index eba482c..bfb7849 100644 --- a/oidc_provider/lib/utils/oauth2.py +++ b/oidc_provider/lib/utils/oauth2.py @@ -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) diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 3f0d2e9..73fd62e 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -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. diff --git a/oidc_provider/management/commands/creatersakey.py b/oidc_provider/management/commands/creatersakey.py index 1dc1a2c..d5d423f 100644 --- a/oidc_provider/management/commands/creatersakey.py +++ b/oidc_provider/management/commands/creatersakey.py @@ -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 diff --git a/oidc_provider/migrations/0001_initial.py b/oidc_provider/migrations/0001_initial.py index ca32b7e..0a24114 100644 --- a/oidc_provider/migrations/0001_initial.py +++ b/oidc_provider/migrations/0001_initial.py @@ -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={ diff --git a/oidc_provider/migrations/0004_remove_userinfo.py b/oidc_provider/migrations/0004_remove_userinfo.py index 33df109..d4208e0 100644 --- a/oidc_provider/migrations/0004_remove_userinfo.py +++ b/oidc_provider/migrations/0004_remove_userinfo.py @@ -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): diff --git a/oidc_provider/migrations/0007_auto_20160111_1844.py b/oidc_provider/migrations/0007_auto_20160111_1844.py index a160cc5..263c4c5 100644 --- a/oidc_provider/migrations/0007_auto_20160111_1844.py +++ b/oidc_provider/migrations/0007_auto_20160111_1844.py @@ -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( diff --git a/oidc_provider/migrations/0011_client_client_type.py b/oidc_provider/migrations/0011_client_client_type.py index 26e9fc3..563096f 100644 --- a/oidc_provider/migrations/0011_client_client_type.py +++ b/oidc_provider/migrations/0011_client_client_type.py @@ -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='Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.', max_length=30), + field=models.CharField( + choices=[(b'confidential', b'Confidential'), (b'public', b'Public')], + default=b'confidential', + help_text='Confidential clients are capable of maintaining the confidentiality of their ' + 'credentials. Public clients are incapable.', + max_length=30), ), ] diff --git a/oidc_provider/migrations/0014_client_jwt_alg.py b/oidc_provider/migrations/0014_client_jwt_alg.py index d2b096c..18a34c2 100644 --- a/oidc_provider/migrations/0014_client_jwt_alg.py +++ b/oidc_provider/migrations/0014_client_jwt_alg.py @@ -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'), ), ] diff --git a/oidc_provider/migrations/0015_change_client_code.py b/oidc_provider/migrations/0015_change_client_code.py index bfffd57..a4f67e1 100644 --- a/oidc_provider/migrations/0015_change_client_code.py +++ b/oidc_provider/migrations/0015_change_client_code.py @@ -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='Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.', max_length=30), + field=models.CharField( + choices=[('confidential', 'Confidential'), ('public', 'Public')], + default='confidential', + help_text='Confidential clients are capable of maintaining the confidentiality of their' + ' credentials. Public 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', diff --git a/oidc_provider/migrations/0016_userconsent_and_verbosenames.py b/oidc_provider/migrations/0016_userconsent_and_verbosenames.py index afd043e..a698362 100644 --- a/oidc_provider/migrations/0016_userconsent_and_verbosenames.py +++ b/oidc_provider/migrations/0016_userconsent_and_verbosenames.py @@ -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='Confidential clients are capable of maintaining the confidentiality of their credentials. Public 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='Confidential clients are capable of maintaining the confidentiality of their ' + 'credentials. Public 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'), ), ] diff --git a/oidc_provider/migrations/0017_auto_20160811_1954.py b/oidc_provider/migrations/0017_auto_20160811_1954.py index de7350f..2d564e3 100644 --- a/oidc_provider/migrations/0017_auto_20160811_1954.py +++ b/oidc_provider/migrations/0017_auto_20160811_1954.py @@ -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='Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.', max_length=30, verbose_name='Client Type'), + field=models.CharField( + choices=[('confidential', 'Confidential'), ('public', 'Public')], + default='confidential', + help_text='Confidential clients are capable of maintaining the confidentiality of their ' + 'credentials. Public 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', diff --git a/oidc_provider/migrations/0018_hybridflow_and_clientattrs.py b/oidc_provider/migrations/0018_hybridflow_and_clientattrs.py index c915cf8..06328dd 100644 --- a/oidc_provider/migrations/0018_hybridflow_and_clientattrs.py +++ b/oidc_provider/migrations/0018_hybridflow_and_clientattrs.py @@ -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'), ), ] diff --git a/oidc_provider/migrations/0020_client__post_logout_redirect_uris.py b/oidc_provider/migrations/0020_client__post_logout_redirect_uris.py index db8e6d6..158da24 100644 --- a/oidc_provider/migrations/0020_client__post_logout_redirect_uris.py +++ b/oidc_provider/migrations/0020_client__post_logout_redirect_uris.py @@ -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'), ), ] diff --git a/oidc_provider/migrations/0022_auto_20170331_1626.py b/oidc_provider/migrations/0022_auto_20170331_1626.py index bad8c93..78b7026 100644 --- a/oidc_provider/migrations/0022_auto_20170331_1626.py +++ b/oidc_provider/migrations/0022_auto_20170331_1626.py @@ -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?'), ), ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index a196239..4e28dde 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -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'Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.')) + client_type = models.CharField( + max_length=30, + choices=CLIENT_TYPE_CHOICES, + default='confidential', + verbose_name=_(u'Client Type'), + help_text=_(u'Confidential clients are capable of maintaining the confidentiality of their credentials. ' + u'Public 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') diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index fc2b4c9..7c7da44 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -145,6 +145,7 @@ class DefaultSettings(object): 'error': 'oidc_provider/error.html' } + default_settings = DefaultSettings() diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 361f27f..b498425 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -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. diff --git a/oidc_provider/tests/test_end_session_endpoint.py b/oidc_provider/tests/test_end_session_endpoint.py index b416762..651c70a 100644 --- a/oidc_provider/tests/test_end_session_endpoint.py +++ b/oidc_provider/tests/test_end_session_endpoint.py @@ -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)) diff --git a/oidc_provider/tests/test_middleware.py b/oidc_provider/tests/test_middleware.py index c2a02df..5a3cc5f 100644 --- a/oidc_provider/tests/test_middleware.py +++ b/oidc_provider/tests/test_middleware.py @@ -10,6 +10,7 @@ class StubbedViews: urlpatterns = [url('^test/', SampleView.as_view())] + MW_CLASSES = ('django.contrib.sessions.middleware.SessionMiddleware', 'oidc_provider.middleware.SessionManagementMiddleware') diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index b23d4fd..49b7598 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -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')) diff --git a/oidc_provider/tests/test_userinfo_endpoint.py b/oidc_provider/tests/test_userinfo_endpoint.py index de95cd8..8ac52c7 100644 --- a/oidc_provider/tests/test_userinfo_endpoint.py +++ b/oidc_provider/tests/test_userinfo_endpoint.py @@ -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.') diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 27d1499..5a4f364 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -78,7 +78,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')) @@ -86,7 +87,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) @@ -156,13 +157,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() @@ -171,7 +174,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']) diff --git a/runtests.py b/runtests.py index 1557853..a8576ce 100644 --- a/runtests.py +++ b/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', ) diff --git a/tox.ini b/tox.ini index ef539ff..e3b52b1 100644 --- a/tox.ini +++ b/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