From 2c37fc5a8c6b3a7c919de08fdd3736295fe83b00 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 28 Jan 2015 17:44:38 -0300 Subject: [PATCH 001/125] Fix README. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2f9167a..bdf0912 100644 --- a/README.rst +++ b/README.rst @@ -17,8 +17,8 @@ Installation Install the package using pip. .. code:: bash - - pip install https://github.com/juanifioren/django-openid-provider/archive/master.zip + + pip install git+https://github.com/juanifioren/django-openid-provider.git#egg=openid_provider Add it to your apps. From 998032ea52bdca2845b6c20e99ead5b52d0a462a Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 29 Jan 2015 12:03:43 -0300 Subject: [PATCH 002/125] Edit README. --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index bdf0912..6b0afd5 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,7 @@ Important things that you should know: - Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. - This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hybrid`` flow at this moment. - Only support for requesting Claims using Scope Values. +- Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. ************ Installation From 8e78e2dc9e3178f59425ed8b079297d546ae2a13 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 29 Jan 2015 12:54:13 -0300 Subject: [PATCH 003/125] Wrong naming in classmethods. Sorry. --- openid_provider/lib/endpoints/token.py | 2 +- openid_provider/lib/endpoints/userinfo.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openid_provider/lib/endpoints/token.py b/openid_provider/lib/endpoints/token.py index 4718514..6f8fbfd 100644 --- a/openid_provider/lib/endpoints/token.py +++ b/openid_provider/lib/endpoints/token.py @@ -84,7 +84,7 @@ class TokenEndpoint(object): return dic @classmethod - def response(self, dic, status=200): + def response(cls, dic, status=200): """ Create and return a response object. """ diff --git a/openid_provider/lib/endpoints/userinfo.py b/openid_provider/lib/endpoints/userinfo.py index 84b56ab..a4b379e 100644 --- a/openid_provider/lib/endpoints/userinfo.py +++ b/openid_provider/lib/endpoints/userinfo.py @@ -64,7 +64,7 @@ class UserInfoEndpoint(object): return dic @classmethod - def response(self, dic): + def response(cls, dic): response = JsonResponse(dic, status=200) response['Cache-Control'] = 'no-store' @@ -73,7 +73,7 @@ class UserInfoEndpoint(object): return response @classmethod - def error_response(self, code, description, status): + def error_response(cls, code, description, status): response = HttpResponse(status=status) response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(code, description) From 564dd29d27ee7b57ccab3c8c41a9838e491e08d3 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 29 Jan 2015 14:03:17 -0300 Subject: [PATCH 004/125] Clean authorize template. Separate hidden inputs. --- .../templates/openid_provider/authorize.html | 8 +------- .../templates/openid_provider/hidden_inputs.html | 5 +++++ openid_provider/views.py | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 openid_provider/templates/openid_provider/hidden_inputs.html diff --git a/openid_provider/templates/openid_provider/authorize.html b/openid_provider/templates/openid_provider/authorize.html index d404d10..a6264bc 100644 --- a/openid_provider/templates/openid_provider/authorize.html +++ b/openid_provider/templates/openid_provider/authorize.html @@ -6,17 +6,11 @@ {% csrf_token %} - - - - - + {{ hidden_inputs }}
    {% for scope in params.scope %} - {% if scope != 'openid' %}
  • {{ scope | capfirst }}
  • - {% endif %} {% endfor %}
diff --git a/openid_provider/templates/openid_provider/hidden_inputs.html b/openid_provider/templates/openid_provider/hidden_inputs.html new file mode 100644 index 0000000..286a188 --- /dev/null +++ b/openid_provider/templates/openid_provider/hidden_inputs.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/openid_provider/views.py b/openid_provider/views.py index dfa3cbd..a80cde0 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -3,6 +3,7 @@ from django.contrib.auth.views import redirect_to_login from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import render +from django.template.loader import render_to_string from django.views.decorators.http import require_http_methods from django.views.generic import View from openid_provider.lib.errors import * @@ -23,12 +24,20 @@ class AuthorizeView(View): if request.user.is_authenticated(): - # This is for printing scopes in form. - authorize.params.scope_str = ' '.join(authorize.params.scope) - + # Generate hidden inputs for the form. context = { 'params': authorize.params, + } + hidden_inputs = render_to_string( + 'openid_provider/hidden_inputs.html', context) + + # Remove openid from scope list since we don't need to print it. + authorize.params.scope.remove('openid') + + context = { 'client': authorize.client, + 'hidden_inputs': hidden_inputs, + 'params': authorize.params, } return render(request, 'openid_provider/authorize.html', context) From 0b6fca3a12794630fdfdcf81a24a6e1dea154a74 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 29 Jan 2015 16:19:48 -0300 Subject: [PATCH 005/125] Edit README. --- README.rst | 70 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 6b0afd5..5a80490 100644 --- a/README.rst +++ b/README.rst @@ -85,11 +85,13 @@ Then let's create a Client. Start django shell: ``python manage.py shell``. >>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/']) >>> c.save() -******************* -/authorize endpoint -******************* +**************** +Server Endpoints +**************** -Example of an OpenID Authentication Request using the ´´Authorization Code´´ flow. +**/authorize endpoint** + +Example of an OpenID Authentication Request using the ``Authorization Code`` flow. .. code:: curl @@ -98,21 +100,15 @@ Example of an OpenID Authentication Request using the ´´Authorization Code´´ Cache-Control: no-cache Content-Type: application/x-www-form-urlencoded -**** -Code -**** - After the user accepts and authorizes the client application, the server redirects to: .. code:: curl http://example.com/?code=5fb3b172913448acadce6b011af1e75e&state=abcdefgh -We extract the ``code`` param and use it to obtain access token. +The ``code`` param will be use it to obtain access token. -*************** -/token endpoint -*************** +**/token endpoint** .. code:: curl @@ -123,12 +119,56 @@ We extract the ``code`` param and use it to obtain access token. client_id=123&client_secret=456&redirect_uri=http%253A%252F%252Fexample.com%252F&grant_type=authorization_code&code=[CODE]&state=abcdefgh -****************** -/userinfo endpoint -****************** +**/userinfo endpoint** .. code:: curl POST /openid/userinfo/ HTTP/1.1 Host: localhost:8000 Authorization: Bearer [ACCESS_TOKEN] + +********* +Templates +********* + +Add your own templates files inside a folder named ``templates/openid_provider/``. +You can copy the sample html here and edit them with your own styles. + +**Authorize Template** + +.. code:: html + +

Request for Permission

+ +

Client {{ client.name }} would like to access this information of you ...

+ +
+ + {% csrf_token %} + + {{ hidden_inputs }} + +
    + {% for scope in params.scope %} +
  • {{ scope | capfirst }}
  • + {% endfor %} +
+ + + +
+ + {% endblock %} + +**Error Template** + +.. code:: html + +

{{ error }}

+

{{ description }}

+ +************ +Contributing +************ + +We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just submit a Pull Request. From 04b927e6fded3073cfb7067fe3bc7bdab6c08020 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 29 Jan 2015 23:32:20 -0300 Subject: [PATCH 006/125] Fix README. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5a80490..e48a31c 100644 --- a/README.rst +++ b/README.rst @@ -134,7 +134,7 @@ Templates Add your own templates files inside a folder named ``templates/openid_provider/``. You can copy the sample html here and edit them with your own styles. -**Authorize Template** +**authorize.html** .. code:: html @@ -160,7 +160,7 @@ You can copy the sample html here and edit them with your own styles. {% endblock %} -**Error Template** +**error.html** .. code:: html From 790c12a53c4ba3d58c7db7319356904afd1fdfaa Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 30 Jan 2015 17:20:36 -0300 Subject: [PATCH 007/125] Add custom scope claims feature. --- README.rst | 51 ++++++++- openid_provider/lib/claims.py | 121 ++++++++++++++++++++++ openid_provider/lib/endpoints/userinfo.py | 19 ++-- openid_provider/lib/scopes.py | 116 --------------------- openid_provider/settings.py | 11 +- openid_provider/views.py | 4 +- 6 files changed, 194 insertions(+), 128 deletions(-) create mode 100644 openid_provider/lib/claims.py delete mode 100644 openid_provider/lib/scopes.py diff --git a/README.rst b/README.rst index 5a80490..1474d00 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ Important things that you should know: - Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. +- Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. - This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hybrid`` flow at this moment. - Only support for requesting Claims using Scope Values. -- Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. ************ Installation @@ -67,6 +67,7 @@ Add required variables to your project settings. # OPTIONAL. DOP_CODE_EXPIRE = 60*10 # 10 min. + DOP_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, DOP_IDTOKEN_EXPIRE = 60*10, # 10 min. DOP_TOKEN_EXPIRE = 60*60 # 1 hour. @@ -127,6 +128,54 @@ The ``code`` param will be use it to obtain access token. Host: localhost:8000 Authorization: Bearer [ACCESS_TOKEN] +*************** +Claims & Scopes +*************** + +OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. + +Here you have the standard scopes defined by the protocol. +http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + +If you need to add extra scopes specific for your app you can add them using the ``DOP_EXTRA_SCOPE_CLAIMS`` settings variable. +This class MUST inherit ``AbstractScopeClaims``. + +Check out an example: + +.. code:: python + + from openid_provider.lib.claims import AbstractScopeClaims + + class MyAppScopeClaims(AbstractScopeClaims): + + def __init__(self, user, scopes): + # Don't forget this. + super(StandardScopeClaims, self).__init__(user, scopes) + + # Here you can load models that will be used + # in more than one scope for example. + try: + self.some_model = SomeModel.objects.get(user=self.user) + except UserInfo.DoesNotExist: + # Create an empty model object. + self.some_model = SomeModel() + + def scope_books(self, user): + + # Here you can search books for this user. + # Remember that you have "self.some_model" also. + + dic = { + 'books_readed': books_readed_count, + } + + return dic + +See how we create our own scopes using the convention ``def scope_(self, user):``. +If a field is empty or ``None`` will be cleaned from the response. + +**Don't forget to add your class into your app settings.** + ********* Templates ********* diff --git a/openid_provider/lib/claims.py b/openid_provider/lib/claims.py new file mode 100644 index 0000000..7b0b97e --- /dev/null +++ b/openid_provider/lib/claims.py @@ -0,0 +1,121 @@ +from django.utils.translation import ugettext as _ +from openid_provider.models import UserInfo + + +class AbstractScopeClaims(object): + + def __init__(self, user, scopes): + self.user = user + self.scopes = scopes + + def create_response_dic(self): + """ + Generate the dic that will be jsonify. Checking scopes given vs + registered. + + Returns a dic. + """ + dic = {} + + for scope in self.scopes: + + if scope in self._scopes_registered(): + dic.update(getattr(self, 'scope_' + scope)(self.user)) + + dic = self._clean_dic(dic) + + return dic + + def _scopes_registered(self): + """ + Return a list that contains all the scopes registered + in the class. + """ + scopes = [] + + for name in self.__class__.__dict__: + + if name.startswith('scope_'): + scope = name.split('scope_')[1] + scopes.append(scope) + + return scopes + + def _clean_dic(self, dic): + """ + Clean recursively all empty or None values inside a dict. + """ + aux_dic = dic.copy() + for key, value in dic.iteritems(): + + if not value: + del aux_dic[key] + elif type(value) is dict: + aux_dic[key] = clean_dic(value) + + return aux_dic + +class StandardScopeClaims(AbstractScopeClaims): + """ + Based on OpenID Standard Claims. + See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + """ + + def __init__(self, user, scopes): + super(StandardScopeClaims, self).__init__(user, scopes) + + try: + self.userinfo = UserInfo.objects.get(user=self.user) + except UserInfo.DoesNotExist: + # Create an empty model object. + self.userinfo = UserInfo() + + def scope_profile(self, user): + dic = { + 'name': self.userinfo.name, + 'given_name': self.userinfo.given_name, + 'family_name': self.userinfo.family_name, + 'middle_name': self.userinfo.middle_name, + 'nickname': self.userinfo.nickname, + 'preferred_username': self.userinfo.preferred_username, + 'profile': self.userinfo.profile, + 'picture': self.userinfo.picture, + 'website': self.userinfo.website, + 'gender': self.userinfo.gender, + 'birthdate': self.userinfo.birthdate, + 'zoneinfo': self.userinfo.zoneinfo, + 'locale': self.userinfo.locale, + 'updated_at': self.userinfo.updated_at, + } + + return dic + + def scope_email(self, user): + dic = { + 'email': self.user.email, + 'email_verified': self.userinfo.email_verified, + } + + return dic + + def scope_phone(self, user): + dic = { + 'phone_number': self.userinfo.phone_number, + 'phone_number_verified': self.userinfo.phone_number_verified, + } + + return dic + + def scope_address(self, user): + dic = { + 'address': { + 'formatted': self.userinfo.address_formatted, + 'street_address': self.userinfo.address_street_address, + 'locality': self.userinfo.address_locality, + 'region': self.userinfo.address_region, + 'postal_code': self.userinfo.address_postal_code, + 'country': self.userinfo.address_country, + } + } + + return dic diff --git a/openid_provider/lib/endpoints/userinfo.py b/openid_provider/lib/endpoints/userinfo.py index a4b379e..41c4b22 100644 --- a/openid_provider/lib/endpoints/userinfo.py +++ b/openid_provider/lib/endpoints/userinfo.py @@ -1,11 +1,11 @@ -import re - -from django.http import HttpResponse, JsonResponse - +from django.http import HttpResponse +from django.http import JsonResponse from openid_provider.lib.errors import * -from openid_provider.lib.scopes import * +from openid_provider.lib.claims import * from openid_provider.lib.utils.params import * from openid_provider.models import * +from openid_provider import settings +import re class UserInfoEndpoint(object): @@ -57,10 +57,15 @@ class UserInfoEndpoint(object): 'sub': self.token.id_token.get('sub'), } - standard_claims = StandardClaims(self.token.user, self.token.scope) - + standard_claims = StandardScopeClaims(self.token.user, self.token.scope) + dic.update(standard_claims.create_response_dic()) + extra_claims = settings.get('DOP_EXTRA_SCOPE_CLAIMS')( + self.token.user, self.token.scope) + + dic.update(extra_claims.create_response_dic()) + return dic @classmethod diff --git a/openid_provider/lib/scopes.py b/openid_provider/lib/scopes.py deleted file mode 100644 index 530d0b7..0000000 --- a/openid_provider/lib/scopes.py +++ /dev/null @@ -1,116 +0,0 @@ -from django.utils.translation import ugettext as _ - -from openid_provider.models import UserInfo - - -# Standard Claims -# http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - -class StandardClaims(object): - - __model__ = UserInfo - - def __init__(self, user, scopes): - self.user = user - self.scopes = scopes - - try: - self.model = self.__model__.objects.get(user=self.user) - except self.__model__.DoesNotExist: - self.model = self.__model__() - - def create_response_dic(self): - - dic = {} - - for scope in self.scopes: - - if scope in self._scopes_registered(): - dic.update(getattr(self, 'scope_' + scope)) - - dic = self._clean_dic(dic) - - return dic - - def _scopes_registered(self): - """ - Return a list that contains all the scopes registered - in the class. - """ - scopes = [] - - for name in self.__class__.__dict__: - - if name.startswith('scope_'): - scope = name.split('scope_')[1] - scopes.append(scope) - - return scopes - - def _clean_dic(self, dic): - """ - Clean recursively all empty or None values inside a dict. - """ - aux_dic = dic.copy() - for key, value in dic.iteritems(): - - if not value: - del aux_dic[key] - elif type(value) is dict: - aux_dic[key] = clean_dic(value) - - return aux_dic - - @property - def scope_profile(self): - dic = { - 'name': self.model.name, - 'given_name': self.model.given_name, - 'family_name': self.model.family_name, - 'middle_name': self.model.middle_name, - 'nickname': self.model.nickname, - 'preferred_username': self.model.preferred_username, - 'profile': self.model.profile, - 'picture': self.model.picture, - 'website': self.model.website, - 'gender': self.model.gender, - 'birthdate': self.model.birthdate, - 'zoneinfo': self.model.zoneinfo, - 'locale': self.model.locale, - 'updated_at': self.model.updated_at, - } - - return dic - - @property - def scope_email(self): - dic = { - 'email': self.user.email, - 'email_verified': self.model.email_verified, - } - - return dic - - @property - def scope_phone(self): - dic = { - 'phone_number': self.model.phone_number, - 'phone_number_verified': self.model.phone_number_verified, - } - - return dic - - @property - def scope_address(self): - dic = { - 'address': { - 'formatted': self.model.address_formatted, - 'street_address': self.model.address_street_address, - 'locality': self.model.address_locality, - 'region': self.model.address_region, - 'postal_code': self.model.address_postal_code, - 'country': self.model.address_country, - } - } - - return dic diff --git a/openid_provider/settings.py b/openid_provider/settings.py index 6accd8c..7348596 100644 --- a/openid_provider/settings.py +++ b/openid_provider/settings.py @@ -1,13 +1,18 @@ from django.conf import settings +from openid_provider.lib.claims import AbstractScopeClaims # Here goes all the package default settings. default_settings = { - 'DOP_CODE_EXPIRE': 60*10, # 10 min. - 'DOP_IDTOKEN_EXPIRE': 60*10, # 10 min. - 'DOP_TOKEN_EXPIRE': 60*60, # 1 hour. + # Required. 'LOGIN_URL': None, 'SITE_URL': None, + + # Optional. + 'DOP_CODE_EXPIRE': 60*10, + 'DOP_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, + 'DOP_IDTOKEN_EXPIRE': 60*10, + 'DOP_TOKEN_EXPIRE': 60*60, } def get(name): diff --git a/openid_provider/views.py b/openid_provider/views.py index a80cde0..d4a7f9b 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -73,7 +73,9 @@ class AuthorizeView(View): return HttpResponseRedirect(uri) except (AuthorizeError) as error: - uri = error.create_uri(authorize.params.redirect_uri, authorize.params.state) + uri = error.create_uri( + authorize.params.redirect_uri, + authorize.params.state) return HttpResponseRedirect(uri) From c5e37e9fdb0c80086250c34df7d425a71d64f7dd Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 2 Feb 2015 17:39:01 -0300 Subject: [PATCH 008/125] Add access_token if token in response_type param. --- openid_provider/lib/endpoints/authorize.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openid_provider/lib/endpoints/authorize.py b/openid_provider/lib/endpoints/authorize.py index ebfbbb9..ca5aa44 100644 --- a/openid_provider/lib/endpoints/authorize.py +++ b/openid_provider/lib/endpoints/authorize.py @@ -102,6 +102,7 @@ class AuthorizeEndpoint(object): code.scope = self.params.scope code.save() + # Create the response uri. uri = self.params.redirect_uri + '?code={0}'.format(code.code) else: # Implicit Flow @@ -123,13 +124,18 @@ class AuthorizeEndpoint(object): id_token = encode_id_token( id_token_dic, self.client.client_secret) - # TODO: Check if response_type is 'id_token token' then + # Create the response uri. + uri = self.params.redirect_uri + \ + '#token_type={0}&id_token={1}&expires_in={2}'.format( + 'bearer', + id_token, + 60 * 10, + ) + + # Check if response_type is 'id_token token' then # add access_token to the fragment. - uri = self.params.redirect_uri + '#token_type={0}&id_token={1}&expires_in={2}'.format( - 'bearer', - id_token, - 60 * 10 - ) + if self.params.response_type == 'id_token token': + uri += '&access_token={0}'.format(token.access_token) except: raise AuthorizeError( self.params.redirect_uri, @@ -137,8 +143,6 @@ class AuthorizeEndpoint(object): self.grant_type) # Add state if present. - uri = uri + \ - ('&state={0}'.format(self.params.state) - if self.params.state else '') + uri += ('&state={0}'.format(self.params.state) if self.params.state else '') return uri From 296a4fecc8fe95607272dc7269560b3a0ee04340 Mon Sep 17 00:00:00 2001 From: Jorge Vazquez Date: Fri, 6 Feb 2015 14:26:34 -0300 Subject: [PATCH 009/125] Auto now --- .gitignore | 3 ++- openid_provider/models.py | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 4f7b4c3..d2b70e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__/ *.py[cod] -*.egg-info \ No newline at end of file +*.egg-info +.ropeproject diff --git a/openid_provider/models.py b/openid_provider/models.py index ff0070c..d0565ac 100644 --- a/openid_provider/models.py +++ b/openid_provider/models.py @@ -16,7 +16,8 @@ class Client(models.Model): name = models.CharField(max_length=100, default='') client_id = models.CharField(max_length=255, unique=True) client_secret = models.CharField(max_length=255, unique=True) - response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES) + response_type = models.CharField(max_length=30, + choices=RESPONSE_TYPE_CHOICES) _redirect_uris = models.TextField(default='') @@ -39,7 +40,7 @@ class Code(models.Model): client = models.ForeignKey(Client) code = models.CharField(max_length=255, unique=True) expires_at = models.DateTimeField() - + _scope = models.TextField(default='') def scope(): @@ -60,7 +61,7 @@ class Token(models.Model): client = models.ForeignKey(Client) access_token = models.CharField(max_length=255, unique=True) expires_at = models.DateTimeField() - + _scope = models.TextField(default='') def scope(): @@ -107,7 +108,7 @@ class UserInfo(models.Model): address_region = models.CharField(max_length=255, default='') address_postal_code = models.CharField(max_length=255, default='') address_country = models.CharField(max_length=255, default='') - updated_at = models.DateTimeField() + updated_at = models.DateTimeField(auto_now=True) @property def name(self): @@ -117,4 +118,4 @@ class UserInfo(models.Model): if self.family_name: name = name + ' ' + self.family_name - return name \ No newline at end of file + return name From 7c49df79889ad18c497f4c0c08f8e6b2dd980b7d Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 9 Feb 2015 17:36:29 -0300 Subject: [PATCH 010/125] Remove unnecessary arguments for redirect_to_login. --- openid_provider/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openid_provider/views.py b/openid_provider/views.py index d4a7f9b..791efd1 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -43,8 +43,7 @@ class AuthorizeView(View): return render(request, 'openid_provider/authorize.html', context) else: path = request.get_full_path() - return redirect_to_login( - path, settings.get('LOGIN_URL'), REDIRECT_FIELD_NAME) + return redirect_to_login(path) except (ClientIdError, RedirectUriError) as error: context = { From 010e27a007a22919c6327ecf98edc8f4b172d8ab Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 9 Feb 2015 17:51:58 -0300 Subject: [PATCH 011/125] Remove unused imports in views. --- openid_provider/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openid_provider/views.py b/openid_provider/views.py index 791efd1..27e7ec9 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -1,4 +1,3 @@ -from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect, JsonResponse @@ -10,7 +9,6 @@ from openid_provider.lib.errors import * from openid_provider.lib.endpoints.authorize import * from openid_provider.lib.endpoints.token import * from openid_provider.lib.endpoints.userinfo import * -from openid_provider import settings class AuthorizeView(View): From 09fbaa7b3dc7931c2ead163678a4deb3c9f0ffa4 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 11 Feb 2015 15:37:51 -0300 Subject: [PATCH 012/125] Add some tests. --- README.rst | 12 ++++++++- openid_provider/tests/__init__.py | 0 openid_provider/tests/test_code_flow.py | 29 ++++++++++++++++++++ openid_provider/tests/utils.py | 36 +++++++++++++++++++++++++ openid_provider/urls.py | 8 +++--- openid_provider/views.py | 1 - setup.py | 6 ++--- 7 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 openid_provider/tests/__init__.py create mode 100644 openid_provider/tests/test_code_flow.py create mode 100644 openid_provider/tests/utils.py diff --git a/README.rst b/README.rst index d542338..4890a62 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Install the package using pip. .. code:: bash - pip install git+https://github.com/juanifioren/django-openid-provider.git#egg=openid_provider + pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=openid_provider Add it to your apps. @@ -216,6 +216,16 @@ You can copy the sample html here and edit them with your own styles.

{{ error }}

{{ description }}

+************* +Running tests +************* + +Just run them as normal Django tests. + +.. code:: bash + + $ python manage.py test openid_provider + ************ Contributing ************ diff --git a/openid_provider/tests/__init__.py b/openid_provider/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openid_provider/tests/test_code_flow.py b/openid_provider/tests/test_code_flow.py new file mode 100644 index 0000000..0aada01 --- /dev/null +++ b/openid_provider/tests/test_code_flow.py @@ -0,0 +1,29 @@ +from django.core.urlresolvers import reverse +from django.test import RequestFactory +from django.test import TestCase +from openid_provider.tests.utils import * +from openid_provider.views import * + + +class CodeFlowTestCase(TestCase): + + def setUp(self): + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='code') + + def test_authorize_invalid_parameters(self): + """ + If the request fails due to a missing, invalid, or mismatching + redirection URI, or if the client identifier is missing or invalid, + the authorization server SHOULD inform the resource owner of the error. + + See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + """ + url = reverse('openid_provider:authorize') + request = self.factory.get(url) + + response = AuthorizeView.as_view()(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(bool(response.content), True) \ No newline at end of file diff --git a/openid_provider/tests/utils.py b/openid_provider/tests/utils.py new file mode 100644 index 0000000..e1629b7 --- /dev/null +++ b/openid_provider/tests/utils.py @@ -0,0 +1,36 @@ +from django.contrib.auth.models import User +from openid_provider.models import * + + +def create_fake_user(): + """ + Create a test user. + + Return a User object. + """ + user = User() + user.username = 'johndoe' + user.email = 'johndoe@example.com' + user.set_password('1234') + + user.save() + + return user + +def create_fake_client(response_type): + """ + Create a test client, response_type argument MUST be: + 'code', 'id_token' or 'id_token token'. + + Return a Client object. + """ + client = Client() + client.name = 'Some Client' + client.client_id = '123' + client.client_secret = '456' + client.response_type = response_type + client.redirect_uris = ['http://example.com/'] + + client.save() + + return client diff --git a/openid_provider/urls.py b/openid_provider/urls.py index 3b0183f..31d485a 100644 --- a/openid_provider/urls.py +++ b/openid_provider/urls.py @@ -1,12 +1,12 @@ from django.conf.urls import patterns, include, url from django.views.decorators.csrf import csrf_exempt - from openid_provider.views import * urlpatterns = patterns('', - url(r'^authorize/$', AuthorizeView.as_view(), name='authorize'), - url(r'^token/$', csrf_exempt(TokenView.as_view()), name='token'), - url(r'^userinfo/$', csrf_exempt(userinfo), name='userinfo'), + + url(r'^authorize/$', AuthorizeView.as_view(), name='authorize'), + url(r'^token/$', csrf_exempt(TokenView.as_view()), name='token'), + url(r'^userinfo/$', csrf_exempt(userinfo), name='userinfo'), ) \ No newline at end of file diff --git a/openid_provider/views.py b/openid_provider/views.py index 27e7ec9..ecb2309 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -1,5 +1,4 @@ from django.contrib.auth.views import redirect_to_login -from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import render from django.template.loader import render_to_string diff --git a/setup.py b/setup.py index a6843c6..cdbf314 100644 --- a/setup.py +++ b/setup.py @@ -8,14 +8,14 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( - name='django-openid-provider', - version='0.1', + name='django-oidc-provider', + version='0.0.0', packages=['openid_provider'], include_package_data=True, license='MIT License', description='A simple OpenID Connect Provider implementation for Djangonauts.', long_description=README, - url='http://github.com/juanifioren/django-openid-provider', + url='http://github.com/juanifioren/django-oidc-provider', author='Juan Ignacio Fiorentino', author_email='juanifioren@gmail.com', classifiers=[ From a3c131776678b8e91e1179cd0f3c3b4b3fbbf6fb Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 11 Feb 2015 17:38:37 -0300 Subject: [PATCH 013/125] Add another test for Code Flow. --- openid_provider/tests/test_code_flow.py | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/openid_provider/tests/test_code_flow.py b/openid_provider/tests/test_code_flow.py index 0aada01..4b0493c 100644 --- a/openid_provider/tests/test_code_flow.py +++ b/openid_provider/tests/test_code_flow.py @@ -3,6 +3,7 @@ from django.test import RequestFactory from django.test import TestCase from openid_provider.tests.utils import * from openid_provider.views import * +import urllib class CodeFlowTestCase(TestCase): @@ -26,4 +27,29 @@ class CodeFlowTestCase(TestCase): response = AuthorizeView.as_view()(request) self.assertEqual(response.status_code, 200) - self.assertEqual(bool(response.content), True) \ No newline at end of file + self.assertEqual(bool(response.content), True) + + def test_authorize_invalid_response_type(self): + """ + The OP informs the RP by using the Error Response parameters defined + in Section 4.1.2.1 of OAuth 2.0. + + See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError + """ + # Create an authorize request with an unsupported response_type. + url = reverse('openid_provider:authorize') + url += '?client_id={0}&response_type=code%20id_token&scope=openid%20email' \ + '&redirect_uri={1}&state=abcdefg'.format( + self.client.client_id, + urllib.quote(self.client.default_redirect_uri), + ) + request = self.factory.get(url) + + response = AuthorizeView.as_view()(request) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.has_header('Location'), True) + + # Check query component in the redirection URI. + correct_query = 'error=' in response['Location'] + self.assertEqual(correct_query, True) \ No newline at end of file From 3161aec2a25563bbf6494bb0b27660406127347d Mon Sep 17 00:00:00 2001 From: Jorge Vazquez Date: Thu, 12 Feb 2015 14:54:38 -0300 Subject: [PATCH 014/125] Fixing birthdate field --- openid_provider/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openid_provider/models.py b/openid_provider/models.py index d0565ac..f52c749 100644 --- a/openid_provider/models.py +++ b/openid_provider/models.py @@ -97,7 +97,7 @@ class UserInfo(models.Model): website = models.URLField(default='') email_verified = models.BooleanField(default=False) gender = models.CharField(max_length=100, default='') - birthdate = models.DateField() + birthdate = models.DateField(null=True) zoneinfo = models.CharField(max_length=100, default='') locale = models.CharField(max_length=100, default='') phone_number = models.CharField(max_length=255, default='') From 7583ac403b05801484bb01376804d9dd197920d9 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 12 Feb 2015 15:04:58 -0300 Subject: [PATCH 015/125] Add more test for the Auth Code Flow. --- openid_provider/tests/test_code_flow.py | 56 +++++++++++++++++++++---- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/openid_provider/tests/test_code_flow.py b/openid_provider/tests/test_code_flow.py index 4b0493c..171b43c 100644 --- a/openid_provider/tests/test_code_flow.py +++ b/openid_provider/tests/test_code_flow.py @@ -1,6 +1,9 @@ +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.models import AnonymousUser from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase +from openid_provider import settings from openid_provider.tests.utils import * from openid_provider.views import * import urllib @@ -13,6 +16,18 @@ class CodeFlowTestCase(TestCase): self.user = create_fake_user() self.client = create_fake_client(response_type='code') + def _create_authorize_url(self, response_type, scope=['openid', 'email']): + url = reverse('openid_provider:authorize') + url += '?client_id={0}&response_type={1}&scope={2}' \ + '&redirect_uri={3}&state=abcdefg'.format( + self.client.client_id, + urllib.quote(response_type), + urllib.quote(' '.join(scope)), + urllib.quote(self.client.default_redirect_uri), + ) + + return url + def test_authorize_invalid_parameters(self): """ If the request fails due to a missing, invalid, or mismatching @@ -37,12 +52,8 @@ class CodeFlowTestCase(TestCase): See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ # Create an authorize request with an unsupported response_type. - url = reverse('openid_provider:authorize') - url += '?client_id={0}&response_type=code%20id_token&scope=openid%20email' \ - '&redirect_uri={1}&state=abcdefg'.format( - self.client.client_id, - urllib.quote(self.client.default_redirect_uri), - ) + url = self._create_authorize_url(response_type='code id_token') + request = self.factory.get(url) response = AuthorizeView.as_view()(request) @@ -50,6 +61,33 @@ class CodeFlowTestCase(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(response.has_header('Location'), True) - # Check query component in the redirection URI. - correct_query = 'error=' in response['Location'] - self.assertEqual(correct_query, True) \ No newline at end of file + # Should be an 'error' component in query. + query_exists = 'error=' in response['Location'] + self.assertEqual(query_exists, True) + + def test_authorize_user_not_logged(self): + """ + The Authorization Server attempts to Authenticate the End-User by + redirecting to the login view. + + See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates + """ + url = self._create_authorize_url(response_type='code') + + request = self.factory.get(url) + request.user = AnonymousUser() + + response = AuthorizeView.as_view()(request) + + # Check if user was redirected to the login view. + login_url_exists = settings.get('LOGIN_URL') in response['Location'] + self.assertEqual(login_url_exists, True) + + # Check if the login will redirect to a valid url. + try: + next_value = response['Location'].split(REDIRECT_FIELD_NAME + '=')[1] + next_url = urllib.unquote(next_value) + is_next_ok = next_url == url + except: + is_next_ok = False + self.assertEqual(is_next_ok, True) \ No newline at end of file From b4fb646f4dfeaff8334f97ea3a4631098a29af07 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 12 Feb 2015 16:56:35 -0300 Subject: [PATCH 016/125] Using urllib module to create url. --- openid_provider/tests/test_code_flow.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openid_provider/tests/test_code_flow.py b/openid_provider/tests/test_code_flow.py index 171b43c..f3fff44 100644 --- a/openid_provider/tests/test_code_flow.py +++ b/openid_provider/tests/test_code_flow.py @@ -17,14 +17,20 @@ class CodeFlowTestCase(TestCase): self.client = create_fake_client(response_type='code') def _create_authorize_url(self, response_type, scope=['openid', 'email']): - url = reverse('openid_provider:authorize') - url += '?client_id={0}&response_type={1}&scope={2}' \ - '&redirect_uri={3}&state=abcdefg'.format( - self.client.client_id, - urllib.quote(response_type), - urllib.quote(' '.join(scope)), - urllib.quote(self.client.default_redirect_uri), - ) + """ + Generate an OpenID Authentication Request using the fake client data. + """ + path = reverse('openid_provider:authorize') + + query_str = urllib.urlencode({ + 'client_id': self.client.client_id, + 'response_type': response_type, + 'redirect_uri': self.client.default_redirect_uri, + 'scope': ' '.join(scope), + 'state': 'abcdefg', + }).replace('+', '%20') + + url = path + '?' + query_str return url From 8dfdf359189f7f56e3cb4d3abbf316cdda23405c Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 12 Feb 2015 21:14:37 -0300 Subject: [PATCH 017/125] Remove img from README. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 4890a62..fc2018f 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -.. image:: http://s1.postimg.org/qcm2dtr6n/title.png -#################################################### +Django-OIDC-Provider +#################### **This project is in ALFA version and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES.** From d526352e28d9008abb273473b202ecf406e129c2 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 13 Feb 2015 10:44:09 -0300 Subject: [PATCH 018/125] Start test for Auth Code Flow. --- openid_provider/tests/test_code_flow.py | 13 ++++++++++++- openid_provider/views.py | 1 - 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openid_provider/tests/test_code_flow.py b/openid_provider/tests/test_code_flow.py index f3fff44..d6ad77b 100644 --- a/openid_provider/tests/test_code_flow.py +++ b/openid_provider/tests/test_code_flow.py @@ -96,4 +96,15 @@ class CodeFlowTestCase(TestCase): is_next_ok = next_url == url except: is_next_ok = False - self.assertEqual(is_next_ok, True) \ No newline at end of file + self.assertEqual(is_next_ok, True) + + def test_authorize_user_consent(self): + url = self._create_authorize_url(response_type='code') + + request = self.factory.get(url) + # Simulate that the user is logged. + request.user = self.user + + response = AuthorizeView.as_view()(request) + + import pdb; pdb.set_trace() \ No newline at end of file diff --git a/openid_provider/views.py b/openid_provider/views.py index ecb2309..4b9c75d 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -75,7 +75,6 @@ class AuthorizeView(View): return HttpResponseRedirect(uri) - class TokenView(View): def post(self, request, *args, **kwargs): From 10c07f472fdf2ea6e193cee07aa9f61217593429 Mon Sep 17 00:00:00 2001 From: Jorge Vazquez Date: Fri, 13 Feb 2015 14:55:05 -0300 Subject: [PATCH 019/125] We have a bug with this model definition, forcing nullable --- openid_provider/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openid_provider/models.py b/openid_provider/models.py index f52c749..f545ac6 100644 --- a/openid_provider/models.py +++ b/openid_provider/models.py @@ -108,7 +108,7 @@ class UserInfo(models.Model): address_region = models.CharField(max_length=255, default='') address_postal_code = models.CharField(max_length=255, default='') address_country = models.CharField(max_length=255, default='') - updated_at = models.DateTimeField(auto_now=True) + updated_at = models.DateTimeField(auto_now=True, null=True) @property def name(self): From 33ed4bb3b705da69d5ec0cda7fb30e90e227cd90 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 18 Feb 2015 14:37:09 -0300 Subject: [PATCH 020/125] Update README. --- README.rst | 216 +---------------------------------------------------- 1 file changed, 2 insertions(+), 214 deletions(-) diff --git a/README.rst b/README.rst index fc2018f..4475d9b 100644 --- a/README.rst +++ b/README.rst @@ -2,219 +2,7 @@ Django-OIDC-Provider #################### -**This project is in ALFA version and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES.** - -Important things that you should know: - -- Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. -- Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. -- This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hybrid`` flow at this moment. -- Only support for requesting Claims using Scope Values. - -************ -Installation -************ - -Install the package using pip. - -.. code:: bash - - pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=openid_provider - - -Add it to your apps. - -.. code:: python - - INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'openid_provider', - # ... - ) - -Add the provider urls. - -.. code:: python - - urlpatterns = patterns('', - # ... - url(r'^openid/', include('openid_provider.urls', namespace='openid_provider')), - # ... - ) - -******** -Settings -******** - -Add required variables to your project settings. - -.. code:: python - - # REQUIRED. - - # Your server provider url. - SITE_URL = 'http://localhost:8000' - - # Used to log the user in. - # See: https://docs.djangoproject.com/en/1.7/ref/settings/#login-url - LOGIN_URL = '/accounts/login/' - - # OPTIONAL. - - DOP_CODE_EXPIRE = 60*10 # 10 min. - DOP_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, - DOP_IDTOKEN_EXPIRE = 60*10, # 10 min. - DOP_TOKEN_EXPIRE = 60*60 # 1 hour. - - -******************** -Create User & Client -******************** - -First of all, we need to create a user: ``python manage.py createsuperuser``. - -Then let's create a Client. Start django shell: ``python manage.py shell``. - -.. code:: python - - >>> from openid_provider.models import Client - >>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/']) - >>> c.save() - -**************** -Server Endpoints -**************** - -**/authorize endpoint** - -Example of an OpenID Authentication Request using the ``Authorization Code`` flow. - -.. code:: curl - - GET /openid/authorize?client_id=123&redirect_uri=http%3A%2F%2Fexample.com%2F&response_type=code&scope=openid%20profile%20email&state=abcdefgh HTTP/1.1 - Host: localhost:8000 - Cache-Control: no-cache - Content-Type: application/x-www-form-urlencoded - -After the user accepts and authorizes the client application, the server redirects to: - -.. code:: curl - - http://example.com/?code=5fb3b172913448acadce6b011af1e75e&state=abcdefgh - -The ``code`` param will be use it to obtain access token. - -**/token endpoint** - -.. code:: curl - - POST /openid/token/ HTTP/1.1 - Host: localhost:8000 - Cache-Control: no-cache - Content-Type: application/x-www-form-urlencoded - - client_id=123&client_secret=456&redirect_uri=http%253A%252F%252Fexample.com%252F&grant_type=authorization_code&code=[CODE]&state=abcdefgh - -**/userinfo endpoint** - -.. code:: curl - - POST /openid/userinfo/ HTTP/1.1 - Host: localhost:8000 - Authorization: Bearer [ACCESS_TOKEN] - -*************** -Claims & Scopes -*************** - -OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. - -Here you have the standard scopes defined by the protocol. -http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims - -If you need to add extra scopes specific for your app you can add them using the ``DOP_EXTRA_SCOPE_CLAIMS`` settings variable. -This class MUST inherit ``AbstractScopeClaims``. - -Check out an example: - -.. code:: python - - from openid_provider.lib.claims import AbstractScopeClaims - - class MyAppScopeClaims(AbstractScopeClaims): - - def __init__(self, user, scopes): - # Don't forget this. - super(StandardScopeClaims, self).__init__(user, scopes) - - # Here you can load models that will be used - # in more than one scope for example. - try: - self.some_model = SomeModel.objects.get(user=self.user) - except UserInfo.DoesNotExist: - # Create an empty model object. - self.some_model = SomeModel() - - def scope_books(self, user): - - # Here you can search books for this user. - # Remember that you have "self.some_model" also. - - dic = { - 'books_readed': books_readed_count, - } - - return dic - -See how we create our own scopes using the convention ``def scope_(self, user):``. -If a field is empty or ``None`` will be cleaned from the response. - -**Don't forget to add your class into your app settings.** - -********* -Templates -********* - -Add your own templates files inside a folder named ``templates/openid_provider/``. -You can copy the sample html here and edit them with your own styles. - -**authorize.html** - -.. code:: html - -

Request for Permission

- -

Client {{ client.name }} would like to access this information of you ...

- -
- - {% csrf_token %} - - {{ hidden_inputs }} - -
    - {% for scope in params.scope %} -
  • {{ scope | capfirst }}
  • - {% endfor %} -
- - - -
- - {% endblock %} - -**error.html** - -.. code:: html - -

{{ error }}

-

{{ description }}

+OpenID Connect Provider implementation for Django. Read docs for more info. https://github.com/juanifioren/django-oidc-provider/wiki ************* Running tests @@ -222,7 +10,7 @@ Running tests Just run them as normal Django tests. -.. code:: bash +.. code:: $ python manage.py test openid_provider From 2bac30361ea9bde691680368eb1a4b7950d1cd50 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 18 Feb 2015 15:07:22 -0300 Subject: [PATCH 021/125] Change name of the package. --- MANIFEST.in | 2 +- README.rst | 18 ++-- .../__init__.py | 0 .../lib/__init__.py | 0 .../lib/claims.py | 2 +- .../lib/endpoints/__init__.py | 0 .../lib/endpoints/authorize.py | 10 +- .../lib/endpoints/token.py | 10 +- .../lib/endpoints/userinfo.py | 10 +- .../lib/errors.py | 0 .../lib/utils/__init__.py | 0 .../lib/utils/params.py | 0 .../lib/utils/token.py | 4 +- oidc_provider/migrations/0001_initial.py | 100 ++++++++++++++++++ .../migrations/__init__.py | 0 {openid_provider => oidc_provider}/models.py | 0 .../settings.py | 2 +- .../templates/oidc_provider}/authorize.html | 2 +- .../templates/oidc_provider}/error.html | 0 .../oidc_provider}/hidden_inputs.html | 0 .../tests/__init__.py | 0 .../tests/test_code_flow.py | 10 +- .../tests/utils.py | 2 +- {openid_provider => oidc_provider}/urls.py | 2 +- {openid_provider => oidc_provider}/views.py | 14 +-- setup.py | 4 +- 26 files changed, 146 insertions(+), 46 deletions(-) rename {openid_provider => oidc_provider}/__init__.py (100%) rename {openid_provider => oidc_provider}/lib/__init__.py (100%) rename {openid_provider => oidc_provider}/lib/claims.py (98%) rename {openid_provider => oidc_provider}/lib/endpoints/__init__.py (100%) rename {openid_provider => oidc_provider}/lib/endpoints/authorize.py (95%) rename {openid_provider => oidc_provider}/lib/endpoints/token.py (92%) rename {openid_provider => oidc_provider}/lib/endpoints/userinfo.py (90%) rename {openid_provider => oidc_provider}/lib/errors.py (100%) rename {openid_provider => oidc_provider}/lib/utils/__init__.py (100%) rename {openid_provider => oidc_provider}/lib/utils/params.py (100%) rename {openid_provider => oidc_provider}/lib/utils/token.py (95%) create mode 100644 oidc_provider/migrations/0001_initial.py rename {openid_provider => oidc_provider}/migrations/__init__.py (100%) rename {openid_provider => oidc_provider}/models.py (100%) rename {openid_provider => oidc_provider}/settings.py (90%) rename {openid_provider/templates/openid_provider => oidc_provider/templates/oidc_provider}/authorize.html (83%) rename {openid_provider/templates/openid_provider => oidc_provider/templates/oidc_provider}/error.html (100%) rename {openid_provider/templates/openid_provider => oidc_provider/templates/oidc_provider}/hidden_inputs.html (100%) rename {openid_provider => oidc_provider}/tests/__init__.py (100%) rename {openid_provider => oidc_provider}/tests/test_code_flow.py (93%) rename {openid_provider => oidc_provider}/tests/utils.py (94%) rename {openid_provider => oidc_provider}/urls.py (90%) rename {openid_provider => oidc_provider}/views.py (87%) diff --git a/MANIFEST.in b/MANIFEST.in index d78e4b3..c4c6761 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE include README.rst -recursive-include openid_provider/templates * \ No newline at end of file +recursive-include oidc_provider/templates * \ No newline at end of file diff --git a/README.rst b/README.rst index fc2018f..8ef2be6 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Django-OIDC-Provider +Django OIDC Provider #################### **This project is in ALFA version and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES.** @@ -19,7 +19,7 @@ Install the package using pip. .. code:: bash - pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=openid_provider + pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=oidc_provider Add it to your apps. @@ -33,7 +33,7 @@ Add it to your apps. 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'openid_provider', + 'oidc_provider', # ... ) @@ -43,7 +43,7 @@ Add the provider urls. urlpatterns = patterns('', # ... - url(r'^openid/', include('openid_provider.urls', namespace='openid_provider')), + url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), # ... ) @@ -82,7 +82,7 @@ Then let's create a Client. Start django shell: ``python manage.py shell``. .. code:: python - >>> from openid_provider.models import Client + >>> from oidc_provider.models import Client >>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/']) >>> c.save() @@ -144,7 +144,7 @@ Check out an example: .. code:: python - from openid_provider.lib.claims import AbstractScopeClaims + from oidc_provider.lib.claims import AbstractScopeClaims class MyAppScopeClaims(AbstractScopeClaims): @@ -180,7 +180,7 @@ If a field is empty or ``None`` will be cleaned from the response. Templates ********* -Add your own templates files inside a folder named ``templates/openid_provider/``. +Add your own templates files inside a folder named ``templates/oidc_provider/``. You can copy the sample html here and edit them with your own styles. **authorize.html** @@ -191,7 +191,7 @@ You can copy the sample html here and edit them with your own styles.

Client {{ client.name }} would like to access this information of you ...

-
+ {% csrf_token %} @@ -224,7 +224,7 @@ Just run them as normal Django tests. .. code:: bash - $ python manage.py test openid_provider + $ python manage.py test oidc_provider ************ Contributing diff --git a/openid_provider/__init__.py b/oidc_provider/__init__.py similarity index 100% rename from openid_provider/__init__.py rename to oidc_provider/__init__.py diff --git a/openid_provider/lib/__init__.py b/oidc_provider/lib/__init__.py similarity index 100% rename from openid_provider/lib/__init__.py rename to oidc_provider/lib/__init__.py diff --git a/openid_provider/lib/claims.py b/oidc_provider/lib/claims.py similarity index 98% rename from openid_provider/lib/claims.py rename to oidc_provider/lib/claims.py index 7b0b97e..3c59bdc 100644 --- a/openid_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -1,5 +1,5 @@ from django.utils.translation import ugettext as _ -from openid_provider.models import UserInfo +from oidc_provider.models import UserInfo class AbstractScopeClaims(object): diff --git a/openid_provider/lib/endpoints/__init__.py b/oidc_provider/lib/endpoints/__init__.py similarity index 100% rename from openid_provider/lib/endpoints/__init__.py rename to oidc_provider/lib/endpoints/__init__.py diff --git a/openid_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py similarity index 95% rename from openid_provider/lib/endpoints/authorize.py rename to oidc_provider/lib/endpoints/authorize.py index ca5aa44..429f74b 100644 --- a/openid_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -1,10 +1,10 @@ from datetime import timedelta from django.utils import timezone -from openid_provider.lib.errors import * -from openid_provider.lib.utils.params import * -from openid_provider.lib.utils.token import * -from openid_provider.models import * -from openid_provider import settings +from oidc_provider.lib.errors import * +from oidc_provider.lib.utils.params import * +from oidc_provider.lib.utils.token import * +from oidc_provider.models import * +from oidc_provider import settings import uuid diff --git a/openid_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py similarity index 92% rename from openid_provider/lib/endpoints/token.py rename to oidc_provider/lib/endpoints/token.py index 6f8fbfd..4d9ba74 100644 --- a/openid_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -1,9 +1,9 @@ from django.http import JsonResponse -from openid_provider.lib.errors import * -from openid_provider.lib.utils.params import * -from openid_provider.lib.utils.token import * -from openid_provider.models import * -from openid_provider import settings +from oidc_provider.lib.errors import * +from oidc_provider.lib.utils.params import * +from oidc_provider.lib.utils.token import * +from oidc_provider.models import * +from oidc_provider import settings import urllib diff --git a/openid_provider/lib/endpoints/userinfo.py b/oidc_provider/lib/endpoints/userinfo.py similarity index 90% rename from openid_provider/lib/endpoints/userinfo.py rename to oidc_provider/lib/endpoints/userinfo.py index 41c4b22..745e00d 100644 --- a/openid_provider/lib/endpoints/userinfo.py +++ b/oidc_provider/lib/endpoints/userinfo.py @@ -1,10 +1,10 @@ from django.http import HttpResponse from django.http import JsonResponse -from openid_provider.lib.errors import * -from openid_provider.lib.claims import * -from openid_provider.lib.utils.params import * -from openid_provider.models import * -from openid_provider import settings +from oidc_provider.lib.errors import * +from oidc_provider.lib.claims import * +from oidc_provider.lib.utils.params import * +from oidc_provider.models import * +from oidc_provider import settings import re diff --git a/openid_provider/lib/errors.py b/oidc_provider/lib/errors.py similarity index 100% rename from openid_provider/lib/errors.py rename to oidc_provider/lib/errors.py diff --git a/openid_provider/lib/utils/__init__.py b/oidc_provider/lib/utils/__init__.py similarity index 100% rename from openid_provider/lib/utils/__init__.py rename to oidc_provider/lib/utils/__init__.py diff --git a/openid_provider/lib/utils/params.py b/oidc_provider/lib/utils/params.py similarity index 100% rename from openid_provider/lib/utils/params.py rename to oidc_provider/lib/utils/params.py diff --git a/openid_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py similarity index 95% rename from openid_provider/lib/utils/token.py rename to oidc_provider/lib/utils/token.py index dc2c859..64d5660 100644 --- a/openid_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -1,7 +1,7 @@ from datetime import timedelta from django.utils import timezone -from openid_provider.models import * -from openid_provider import settings +from oidc_provider.models import * +from oidc_provider import settings import jwt import time import uuid diff --git a/oidc_provider/migrations/0001_initial.py b/oidc_provider/migrations/0001_initial.py new file mode 100644 index 0000000..661e032 --- /dev/null +++ b/oidc_provider/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Client', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('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)')])), + ('_redirect_uris', models.TextField(default=b'')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Code', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('code', models.CharField(unique=True, max_length=255)), + ('expires_at', models.DateTimeField()), + ('_scope', models.TextField(default=b'')), + ('client', models.ForeignKey(to='oidc_provider.Client')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Token', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('access_token', models.CharField(unique=True, max_length=255)), + ('expires_at', models.DateTimeField()), + ('_scope', models.TextField(default=b'')), + ('_id_token', models.TextField()), + ('client', models.ForeignKey(to='oidc_provider.Client')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='UserInfo', + fields=[ + ('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('given_name', models.CharField(default=b'', max_length=255)), + ('family_name', models.CharField(default=b'', max_length=255)), + ('middle_name', models.CharField(default=b'', max_length=255)), + ('nickname', models.CharField(default=b'', max_length=255)), + ('preferred_username', models.CharField(default=b'', max_length=255)), + ('profile', models.URLField(default=b'')), + ('picture', models.URLField(default=b'')), + ('website', models.URLField(default=b'')), + ('email_verified', models.BooleanField(default=False)), + ('gender', models.CharField(default=b'', max_length=100)), + ('birthdate', models.DateField(null=True)), + ('zoneinfo', models.CharField(default=b'', max_length=100)), + ('locale', models.CharField(default=b'', max_length=100)), + ('phone_number', models.CharField(default=b'', max_length=255)), + ('phone_number_verified', models.BooleanField(default=False)), + ('address_formatted', models.CharField(default=b'', max_length=255)), + ('address_street_address', models.CharField(default=b'', max_length=255)), + ('address_locality', models.CharField(default=b'', max_length=255)), + ('address_region', models.CharField(default=b'', max_length=255)), + ('address_postal_code', models.CharField(default=b'', max_length=255)), + ('address_country', models.CharField(default=b'', max_length=255)), + ('updated_at', models.DateTimeField(auto_now=True, null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='token', + name='user', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + migrations.AddField( + model_name='code', + name='user', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + ] diff --git a/openid_provider/migrations/__init__.py b/oidc_provider/migrations/__init__.py similarity index 100% rename from openid_provider/migrations/__init__.py rename to oidc_provider/migrations/__init__.py diff --git a/openid_provider/models.py b/oidc_provider/models.py similarity index 100% rename from openid_provider/models.py rename to oidc_provider/models.py diff --git a/openid_provider/settings.py b/oidc_provider/settings.py similarity index 90% rename from openid_provider/settings.py rename to oidc_provider/settings.py index 7348596..6add2d6 100644 --- a/openid_provider/settings.py +++ b/oidc_provider/settings.py @@ -1,5 +1,5 @@ from django.conf import settings -from openid_provider.lib.claims import AbstractScopeClaims +from oidc_provider.lib.claims import AbstractScopeClaims # Here goes all the package default settings. diff --git a/openid_provider/templates/openid_provider/authorize.html b/oidc_provider/templates/oidc_provider/authorize.html similarity index 83% rename from openid_provider/templates/openid_provider/authorize.html rename to oidc_provider/templates/oidc_provider/authorize.html index a6264bc..69cede7 100644 --- a/openid_provider/templates/openid_provider/authorize.html +++ b/oidc_provider/templates/oidc_provider/authorize.html @@ -2,7 +2,7 @@

Client {{ client.name }} would like to access this information of you ...

- + {% csrf_token %} diff --git a/openid_provider/templates/openid_provider/error.html b/oidc_provider/templates/oidc_provider/error.html similarity index 100% rename from openid_provider/templates/openid_provider/error.html rename to oidc_provider/templates/oidc_provider/error.html diff --git a/openid_provider/templates/openid_provider/hidden_inputs.html b/oidc_provider/templates/oidc_provider/hidden_inputs.html similarity index 100% rename from openid_provider/templates/openid_provider/hidden_inputs.html rename to oidc_provider/templates/oidc_provider/hidden_inputs.html diff --git a/openid_provider/tests/__init__.py b/oidc_provider/tests/__init__.py similarity index 100% rename from openid_provider/tests/__init__.py rename to oidc_provider/tests/__init__.py diff --git a/openid_provider/tests/test_code_flow.py b/oidc_provider/tests/test_code_flow.py similarity index 93% rename from openid_provider/tests/test_code_flow.py rename to oidc_provider/tests/test_code_flow.py index d6ad77b..d43ad39 100644 --- a/openid_provider/tests/test_code_flow.py +++ b/oidc_provider/tests/test_code_flow.py @@ -3,9 +3,9 @@ from django.contrib.auth.models import AnonymousUser from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase -from openid_provider import settings -from openid_provider.tests.utils import * -from openid_provider.views import * +from oidc_provider import settings +from oidc_provider.tests.utils import * +from oidc_provider.views import * import urllib @@ -20,7 +20,7 @@ class CodeFlowTestCase(TestCase): """ Generate an OpenID Authentication Request using the fake client data. """ - path = reverse('openid_provider:authorize') + path = reverse('oidc_provider:authorize') query_str = urllib.urlencode({ 'client_id': self.client.client_id, @@ -42,7 +42,7 @@ class CodeFlowTestCase(TestCase): See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 """ - url = reverse('openid_provider:authorize') + url = reverse('oidc_provider:authorize') request = self.factory.get(url) response = AuthorizeView.as_view()(request) diff --git a/openid_provider/tests/utils.py b/oidc_provider/tests/utils.py similarity index 94% rename from openid_provider/tests/utils.py rename to oidc_provider/tests/utils.py index e1629b7..1d61339 100644 --- a/openid_provider/tests/utils.py +++ b/oidc_provider/tests/utils.py @@ -1,5 +1,5 @@ from django.contrib.auth.models import User -from openid_provider.models import * +from oidc_provider.models import * def create_fake_user(): diff --git a/openid_provider/urls.py b/oidc_provider/urls.py similarity index 90% rename from openid_provider/urls.py rename to oidc_provider/urls.py index 31d485a..54c4f62 100644 --- a/openid_provider/urls.py +++ b/oidc_provider/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import patterns, include, url from django.views.decorators.csrf import csrf_exempt -from openid_provider.views import * +from oidc_provider.views import * urlpatterns = patterns('', diff --git a/openid_provider/views.py b/oidc_provider/views.py similarity index 87% rename from openid_provider/views.py rename to oidc_provider/views.py index 4b9c75d..7b4a693 100644 --- a/openid_provider/views.py +++ b/oidc_provider/views.py @@ -4,10 +4,10 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.views.decorators.http import require_http_methods from django.views.generic import View -from openid_provider.lib.errors import * -from openid_provider.lib.endpoints.authorize import * -from openid_provider.lib.endpoints.token import * -from openid_provider.lib.endpoints.userinfo import * +from oidc_provider.lib.errors import * +from oidc_provider.lib.endpoints.authorize import * +from oidc_provider.lib.endpoints.token import * +from oidc_provider.lib.endpoints.userinfo import * class AuthorizeView(View): @@ -26,7 +26,7 @@ class AuthorizeView(View): 'params': authorize.params, } hidden_inputs = render_to_string( - 'openid_provider/hidden_inputs.html', context) + 'oidc_provider/hidden_inputs.html', context) # Remove openid from scope list since we don't need to print it. authorize.params.scope.remove('openid') @@ -37,7 +37,7 @@ class AuthorizeView(View): 'params': authorize.params, } - return render(request, 'openid_provider/authorize.html', context) + return render(request, 'oidc_provider/authorize.html', context) else: path = request.get_full_path() return redirect_to_login(path) @@ -48,7 +48,7 @@ class AuthorizeView(View): 'description': error.description, } - return render(request, 'openid_provider/error.html', context) + return render(request, 'oidc_provider/error.html', context) except (AuthorizeError) as error: uri = error.create_uri( diff --git a/setup.py b/setup.py index cdbf314..6b9594a 100644 --- a/setup.py +++ b/setup.py @@ -10,10 +10,10 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', version='0.0.0', - packages=['openid_provider'], + packages=['oidc_provider'], include_package_data=True, license='MIT License', - description='A simple OpenID Connect Provider implementation for Djangonauts.', + description='OpenID Connect Provider implementation for Django.', long_description=README, url='http://github.com/juanifioren/django-oidc-provider', author='Juan Ignacio Fiorentino', From c44309c540520b4020d9559f6d93f0a1cab8e811 Mon Sep 17 00:00:00 2001 From: Jorge Vazquez Date: Wed, 18 Feb 2015 16:23:00 -0300 Subject: [PATCH 022/125] Model definition changes --- oidc_provider/models.py | 42 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index f545ac6..d741ce2 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -86,28 +86,26 @@ class Token(models.Model): class UserInfo(models.Model): user = models.OneToOneField(User, primary_key=True) - - given_name = models.CharField(max_length=255, default='') - family_name = models.CharField(max_length=255, default='') - middle_name = models.CharField(max_length=255, default='') - nickname = models.CharField(max_length=255, default='') - preferred_username = models.CharField(max_length=255, default='') - profile = models.URLField(default='') - picture = models.URLField(default='') - website = models.URLField(default='') - email_verified = models.BooleanField(default=False) - gender = models.CharField(max_length=100, default='') - birthdate = models.DateField(null=True) - zoneinfo = models.CharField(max_length=100, default='') - locale = models.CharField(max_length=100, default='') - phone_number = models.CharField(max_length=255, default='') - phone_number_verified = models.BooleanField(default=False) - address_formatted = models.CharField(max_length=255, default='') - address_street_address = models.CharField(max_length=255, default='') - address_locality = models.CharField(max_length=255, default='') - address_region = models.CharField(max_length=255, default='') - address_postal_code = models.CharField(max_length=255, default='') - address_country = models.CharField(max_length=255, default='') + given_name = models.CharField(max_length=255, blank=True, null=True) + family_name = models.CharField(max_length=255, blank=True, null=True) + middle_name = models.CharField(max_length=255, blank=True, null=True) + nickname = models.CharField(max_length=255, blank=True, null=True) + preferred_username = models.CharField(max_length=255, blank=True, null=True) + profile = models.URLField(default='', null=True, blank=True) + picture = models.URLField(default='', null=True, blank=True) + website = models.URLField(default='', null=True, blank=True) + email_verified = models.NullBooleanField(default=False) + locale = models.CharField(max_length=100, blank=True, null=True) + phone_number = models.CharField(max_length=255, blank=True, null=True) + phone_number_verified = models.NullBooleanField(default=False) + address_formatted = models.CharField(max_length=255, blank=True, null=True) + address_street_address = models.CharField(max_length=255, blank=True, + null=True) + address_locality = models.CharField(max_length=255, blank=True, null=True) + address_region = models.CharField(max_length=255, blank=True, null=True) + address_postal_code = models.CharField(max_length=255, blank=True, + null=True) + address_country = models.CharField(max_length=255, blank=True, null=True) updated_at = models.DateTimeField(auto_now=True, null=True) @property From b1e0c9681a87f18adec8d7bfda8874c364a24e7b Mon Sep 17 00:00:00 2001 From: Nicco Date: Thu, 19 Feb 2015 14:29:27 -0300 Subject: [PATCH 023/125] Update setup.py adding tests_require option ;-) --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6b9594a..1638741 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,9 @@ setup( url='http://github.com/juanifioren/django-oidc-provider', author='Juan Ignacio Fiorentino', author_email='juanifioren@gmail.com', + + tests_require=[], + classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', @@ -33,4 +36,4 @@ setup( install_requires=[ 'pyjwt==0.3.1', ], -) \ No newline at end of file +) From dedc70b05a2c0a93e41603162edbe5a626edcd4e Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 19 Feb 2015 15:45:51 -0300 Subject: [PATCH 024/125] Edit tests. --- README.rst | 2 +- ...low.py => test_authorization_code_flow.py} | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) rename oidc_provider/tests/{test_code_flow.py => test_authorization_code_flow.py} (83%) diff --git a/README.rst b/README.rst index 5368202..69e8f57 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ OpenID Connect Provider implementation for Django. Read docs for more info. http Running tests ************* -Just run them as normal Django tests. +You need a Django project properly configured with the package. Then just run tests as normal. .. code:: diff --git a/oidc_provider/tests/test_code_flow.py b/oidc_provider/tests/test_authorization_code_flow.py similarity index 83% rename from oidc_provider/tests/test_code_flow.py rename to oidc_provider/tests/test_authorization_code_flow.py index d43ad39..8de1662 100644 --- a/oidc_provider/tests/test_code_flow.py +++ b/oidc_provider/tests/test_authorization_code_flow.py @@ -9,7 +9,7 @@ from oidc_provider.views import * import urllib -class CodeFlowTestCase(TestCase): +class AuthorizationCodeFlowTestCase(TestCase): def setUp(self): self.factory = RequestFactory() @@ -99,7 +99,9 @@ class CodeFlowTestCase(TestCase): self.assertEqual(is_next_ok, True) def test_authorize_user_consent(self): - url = self._create_authorize_url(response_type='code') + response_type = 'code' + + url = self._create_authorize_url(response_type=response_type) request = self.factory.get(url) # Simulate that the user is logged. @@ -107,4 +109,18 @@ class CodeFlowTestCase(TestCase): response = AuthorizeView.as_view()(request) - import pdb; pdb.set_trace() \ No newline at end of file + # Check if hidden inputs exists in the form, also + # check if their values are valid. + + input_html = '' + + to_check = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': response_type, + } + + for key, value in to_check.iteritems(): + is_input_ok = input_html.format(key, value) in response.content + self.assertEqual(is_input_ok, True, + msg='Hidden input for "'+key+'" fails.') From e40a62cecc9e8d4192bedb3ddcd091554051ab7f Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 20 Feb 2015 14:33:18 -0300 Subject: [PATCH 025/125] Add doc to tests. --- README.rst | 4 +++- oidc_provider/tests/test_authorization_code_flow.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 69e8f57..fa3719b 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,9 @@ Django OIDC Provider #################### -OpenID Connect Provider implementation for Django. Read docs for more info. https://github.com/juanifioren/django-oidc-provider/wiki +OpenID Connect Provider implementation for Django. + +Read docs for more info. https://github.com/juanifioren/django-oidc-provider/wiki ************* Running tests diff --git a/oidc_provider/tests/test_authorization_code_flow.py b/oidc_provider/tests/test_authorization_code_flow.py index 8de1662..8d118bf 100644 --- a/oidc_provider/tests/test_authorization_code_flow.py +++ b/oidc_provider/tests/test_authorization_code_flow.py @@ -99,6 +99,13 @@ class AuthorizationCodeFlowTestCase(TestCase): self.assertEqual(is_next_ok, True) def test_authorize_user_consent(self): + """ + Once the End-User is authenticated, the Authorization Server MUST + obtain an authorization decision before releasing information to + the Client. + + See: http://openid.net/specs/openid-connect-core-1_0.html#Consent + """ response_type = 'code' url = self._create_authorize_url(response_type=response_type) From 0b10f94a0663185a4bfdaae65d73e4977bca65a0 Mon Sep 17 00:00:00 2001 From: Jorge Vazquez Date: Mon, 23 Feb 2015 15:02:26 -0300 Subject: [PATCH 026/125] Adding removed fields that are required by the specs --- oidc_provider/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index d741ce2..9bd31d4 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -90,6 +90,11 @@ class UserInfo(models.Model): family_name = models.CharField(max_length=255, blank=True, null=True) middle_name = models.CharField(max_length=255, blank=True, null=True) nickname = models.CharField(max_length=255, blank=True, null=True) + gender = models.CharField(max_length=100, default='', blank=True, null=True) + birthdate = models.DateField(null=True) + zoneinfo = models.CharField(max_length=100, default='', blank=True, + null=True) + locale = models.CharField(max_length=100, default='', blank=True, null=True) preferred_username = models.CharField(max_length=255, blank=True, null=True) profile = models.URLField(default='', null=True, blank=True) picture = models.URLField(default='', null=True, blank=True) From 9962210cb63e85597ed060f50de5e73b85be3392 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 26 Feb 2015 16:12:58 -0300 Subject: [PATCH 027/125] Add pip dist folder to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d2b70e8..61d98f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ +dist/ *.py[cod] *.egg-info .ropeproject From 9bcdc0e37a7f151df3df26fcac99849fe0f995c4 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 26 Feb 2015 16:13:55 -0300 Subject: [PATCH 028/125] Remove tests_require in setup.py. --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 1638741..9e70fb7 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,6 @@ setup( url='http://github.com/juanifioren/django-oidc-provider', author='Juan Ignacio Fiorentino', author_email='juanifioren@gmail.com', - - tests_require=[], - classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', From 94ccfc54cf1c04c63bc69c946f4e7aadad47fc11 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 26 Feb 2015 16:14:36 -0300 Subject: [PATCH 029/125] Change "DOP" with "OIDC" in settings. --- oidc_provider/lib/endpoints/authorize.py | 2 +- oidc_provider/lib/endpoints/token.py | 2 +- oidc_provider/lib/endpoints/userinfo.py | 2 +- oidc_provider/lib/utils/token.py | 4 ++-- oidc_provider/settings.py | 9 +++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 429f74b..b6ca2fe 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -98,7 +98,7 @@ class AuthorizeEndpoint(object): code.client = self.client code.code = uuid.uuid4().hex code.expires_at = timezone.now() + timedelta( - seconds=settings.get('DOP_CODE_EXPIRE')) + seconds=settings.get('OIDC_CODE_EXPIRE')) code.scope = self.params.scope code.save() diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 4d9ba74..87f73f1 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -77,7 +77,7 @@ class TokenEndpoint(object): dic = { 'access_token': token.access_token, 'token_type': 'bearer', - 'expires_in': settings.get('DOP_TOKEN_EXPIRE'), + 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': id_token, } diff --git a/oidc_provider/lib/endpoints/userinfo.py b/oidc_provider/lib/endpoints/userinfo.py index 745e00d..163dfd5 100644 --- a/oidc_provider/lib/endpoints/userinfo.py +++ b/oidc_provider/lib/endpoints/userinfo.py @@ -61,7 +61,7 @@ class UserInfoEndpoint(object): dic.update(standard_claims.create_response_dic()) - extra_claims = settings.get('DOP_EXTRA_SCOPE_CLAIMS')( + extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS')( self.token.user, self.token.scope) dic.update(extra_claims.create_response_dic()) diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 64d5660..d24ac7c 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -15,7 +15,7 @@ def create_id_token_dic(user, iss, aud): Return a dic. """ - expires_in = settings.get('DOP_IDTOKEN_EXPIRE') + expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') now = timezone.now() @@ -62,7 +62,7 @@ def create_token(user, client, id_token_dic, scope): token.refresh_token = uuid.uuid4().hex token.expires_at = timezone.now() + timedelta( - seconds=settings.get('DOP_TOKEN_EXPIRE')) + seconds=settings.get('OIDC_TOKEN_EXPIRE')) token.scope = scope return token diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 6add2d6..f557d39 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -3,16 +3,17 @@ from oidc_provider.lib.claims import AbstractScopeClaims # Here goes all the package default settings. + default_settings = { # Required. 'LOGIN_URL': None, 'SITE_URL': None, # Optional. - 'DOP_CODE_EXPIRE': 60*10, - 'DOP_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, - 'DOP_IDTOKEN_EXPIRE': 60*10, - 'DOP_TOKEN_EXPIRE': 60*60, + 'OIDC_CODE_EXPIRE': 60*10, + 'OIDC_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, + 'OIDC_IDTOKEN_EXPIRE': 60*10, + 'OIDC_TOKEN_EXPIRE': 60*60, } def get(name): From d1c1e2adb3331a05cc54411a0d436e9a9018d56c Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 26 Feb 2015 16:44:31 -0300 Subject: [PATCH 030/125] Add doc file. --- doc.md | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 doc.md diff --git a/doc.md b/doc.md new file mode 100644 index 0000000..301dc78 --- /dev/null +++ b/doc.md @@ -0,0 +1,224 @@ +# Welcome to the Docs! + +Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. + +**This project is still in DEVELOPMENT and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES, unless you know what you do.** + +Before getting started there are some important things that you should know: +* Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. +* Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. +* This cover authorization_code flow and implicit flow, NO support for hybrid flow at this moment. +* Only support for requesting Claims using Scope Values. + +# Table Of Contents + +- [Installation](#installation) +- [Settings](#settings) +- [Users And Clients](#users-and-clients) +- [Templates](#templates) +- [Server Endpoints](#server-endpoints) +- [Claims And Scopes](#claims-and-scopes) + +## Installation + +Install the package using pip. + +```bash +pip install django-oidc-provider +# Or latest code from repo. +pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=openid_provider +``` + +Add it to your apps. + +```python +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'openid_provider', + # ... +) +``` + +Add the provider urls. + +```python +urlpatterns = patterns('', + # ... + url(r'^openid/', include('openid_provider.urls', namespace='openid_provider')), + # ... +) +``` + +## Settings + +Add required variables to your project settings. + +```python +# REQUIRED SETTINGS. + +# Your server provider url. +SITE_URL = 'http://localhost:8000' + +# Used to log the user in. +# See: https://docs.djangoproject.com/en/1.7/ref/settings/#login-url +LOGIN_URL = '/accounts/login/' + +# OPTIONAL SETTINGS. + +DOP_CODE_EXPIRE = 60*10 # 10 min. +DOP_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, +DOP_IDTOKEN_EXPIRE = 60*10, # 10 min. +DOP_TOKEN_EXPIRE = 60*60 # 1 hour. +``` + +## Users And Clients + +User and client creation it's up to you. This is because is out of the scope in the core implementation of OIDC. +So, there are different ways to create your Clients. By displaying a HTML form or maybe if you have internal thrusted Clients you can create them programatically. + +[Read more about client creation](http://tools.ietf.org/html/rfc6749#section-2). + +For your users, the tipical situation is that you provide them a login and a registration page. + +If you want to test the provider without getting to deep into this topics you can: + +Create a user with: ``python manage.py createsuperuser``. + +And then create a Client with django shell: ``python manage.py shell``. + +```python +>>> from oidc_provider.models import Client +>>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/']) +>>> c.save() +``` + +## Templates + +Add your own templates files inside a folder named ``templates/oidc_provider/``. +You can copy the sample html here and edit them with your own styles. + +**authorize.html** + +```html +

Request for Permission

+ +

Client {{ client.name }} would like to access this information of you ...

+ + + + {% csrf_token %} + + {{ hidden_inputs }} + +
    + {% for scope in params.scope %} +
  • {{ scope | capfirst }}
  • + {% endfor %} +
+ + + + + +{% endblock %} +``` + +**error.html** + +```html +

{{ error }}

+

{{ description }}

+``` + +## Server Endpoints + +**/authorize endpoint** + +Example of an OpenID Authentication Request using the ``Authorization Code`` flow. + +```curl +GET /openid/authorize?client_id=123&redirect_uri=http%3A%2F%2Fexample.com%2F&response_type=code&scope=openid%20profile%20email&state=abcdefgh HTTP/1.1 +Host: localhost:8000 +Cache-Control: no-cache +Content-Type: application/x-www-form-urlencoded +``` + +After the user accepts and authorizes the client application, the server redirects to: + +```curl +http://example.com/?code=5fb3b172913448acadce6b011af1e75e&state=abcdefgh +``` + +The ``code`` param will be use it to obtain access token. + +**/token endpoint** + +```curl +POST /openid/token/ HTTP/1.1 +Host: localhost:8000 +Cache-Control: no-cache +Content-Type: application/x-www-form-urlencoded + client_id=123&client_secret=456&redirect_uri=http%253A%252F%252Fexample.com%252F&grant_type=authorization_code&code=[CODE]&state=abcdefgh +``` + +**/userinfo endpoint** + +```curl +POST /openid/userinfo/ HTTP/1.1 +Host: localhost:8000 +Authorization: Bearer [ACCESS_TOKEN] +``` + +## Claims And Scopes + +OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. + +Here you have the standard scopes defined by the protocol. +http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + +If you need to add extra scopes specific for your app you can add them using the ``DOP_EXTRA_SCOPE_CLAIMS`` settings variable. +This class MUST inherit ``AbstractScopeClaims``. + +Check out an example: + +```python +from openid_provider.lib.claims import AbstractScopeClaims + +class MyAppScopeClaims(AbstractScopeClaims): + + def __init__(self, user, scopes): + # Don't forget this. + super(StandardScopeClaims, self).__init__(user, scopes) + + # Here you can load models that will be used + # in more than one scope for example. + try: + self.some_model = SomeModel.objects.get(user=self.user) + except UserInfo.DoesNotExist: + # Create an empty model object. + self.some_model = SomeModel() + + def scope_books(self, user): + + # Here you can search books for this user. + # Remember that you have "self.some_model" also. + + dic = { + 'books_readed': books_readed_count, + } + + return dic +``` + +See how we create our own scopes using the convention: + +``def scope_(self, user):`` + +If a field is empty or ``None`` will be cleaned from the response. + +**Don't forget to add your class into your app settings.** \ No newline at end of file From cbbd1bdb0ed0750f537a8ba2ee5a9b78bda7c3fb Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 26 Feb 2015 16:51:54 -0300 Subject: [PATCH 031/125] Update README. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index fa3719b..fde0bbc 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,9 @@ Django OIDC Provider #################### -OpenID Connect Provider implementation for Django. +Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. -Read docs for more info. https://github.com/juanifioren/django-oidc-provider/wiki +Read docs for more info. https://github.com/juanifioren/django-oidc-provider/blob/master/doc.md ************* Running tests From 9ed8df3f96d0291db13835afba235d01b3d1f829 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 26 Feb 2015 19:20:30 -0300 Subject: [PATCH 032/125] Update Docs. --- doc.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc.md b/doc.md index 301dc78..894e805 100644 --- a/doc.md +++ b/doc.md @@ -70,10 +70,10 @@ LOGIN_URL = '/accounts/login/' # OPTIONAL SETTINGS. -DOP_CODE_EXPIRE = 60*10 # 10 min. -DOP_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, -DOP_IDTOKEN_EXPIRE = 60*10, # 10 min. -DOP_TOKEN_EXPIRE = 60*60 # 1 hour. +OIDC_CODE_EXPIRE = 60*10 # 10 min. +OIDC_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, +OIDC_IDTOKEN_EXPIRE = 60*10, # 10 min. +OIDC_TOKEN_EXPIRE = 60*60 # 1 hour. ``` ## Users And Clients @@ -221,4 +221,4 @@ See how we create our own scopes using the convention: If a field is empty or ``None`` will be cleaned from the response. -**Don't forget to add your class into your app settings.** \ No newline at end of file +**Don't forget to add your class into your app settings.** From 4628e6f714a568e43ed3b91e2185b9fe74ae7ad3 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 27 Feb 2015 14:21:45 -0300 Subject: [PATCH 033/125] Update README. --- doc.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc.md b/doc.md index 894e805..5a69d89 100644 --- a/doc.md +++ b/doc.md @@ -26,7 +26,7 @@ Install the package using pip. ```bash pip install django-oidc-provider # Or latest code from repo. -pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=openid_provider +pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=oidc_provider ``` Add it to your apps. @@ -39,7 +39,7 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'openid_provider', + 'oidc_provider', # ... ) ``` @@ -49,7 +49,7 @@ Add the provider urls. ```python urlpatterns = patterns('', # ... - url(r'^openid/', include('openid_provider.urls', namespace='openid_provider')), + url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), # ... ) ``` From 6bd66988c31e06c2e6e3cc39d7d40df2ba92834f Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 27 Feb 2015 14:24:43 -0300 Subject: [PATCH 034/125] Update README. --- doc.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc.md b/doc.md index 5a69d89..5f3ce83 100644 --- a/doc.md +++ b/doc.md @@ -12,6 +12,7 @@ Before getting started there are some important things that you should know: # Table Of Contents +- [Requirements](#requirements) - [Installation](#installation) - [Settings](#settings) - [Users And Clients](#users-and-clients) @@ -19,6 +20,11 @@ Before getting started there are some important things that you should know: - [Server Endpoints](#server-endpoints) - [Claims And Scopes](#claims-and-scopes) +## Requirements + +- Python 2.7.*. +- Django 1.7.*. + ## Installation Install the package using pip. From 07c92e81829073cf8d19c4b17dd8e13d1718a69b Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 27 Feb 2015 17:40:17 -0300 Subject: [PATCH 035/125] Complete some tests. Also change a few things on them. --- ...ode_flow.py => test_authorize_endpoint.py} | 130 ++++++++++++++---- 1 file changed, 101 insertions(+), 29 deletions(-) rename oidc_provider/tests/{test_authorization_code_flow.py => test_authorize_endpoint.py} (53%) diff --git a/oidc_provider/tests/test_authorization_code_flow.py b/oidc_provider/tests/test_authorize_endpoint.py similarity index 53% rename from oidc_provider/tests/test_authorization_code_flow.py rename to oidc_provider/tests/test_authorize_endpoint.py index 8d118bf..9122235 100644 --- a/oidc_provider/tests/test_authorization_code_flow.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -1,12 +1,15 @@ +import urllib + from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import AnonymousUser from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase + from oidc_provider import settings +from oidc_provider.models import * from oidc_provider.tests.utils import * from oidc_provider.views import * -import urllib class AuthorizationCodeFlowTestCase(TestCase): @@ -15,26 +18,9 @@ class AuthorizationCodeFlowTestCase(TestCase): self.factory = RequestFactory() self.user = create_fake_user() self.client = create_fake_client(response_type='code') + self.state = uuid.uuid4().hex - def _create_authorize_url(self, response_type, scope=['openid', 'email']): - """ - Generate an OpenID Authentication Request using the fake client data. - """ - path = reverse('oidc_provider:authorize') - - query_str = urllib.urlencode({ - 'client_id': self.client.client_id, - 'response_type': response_type, - 'redirect_uri': self.client.default_redirect_uri, - 'scope': ' '.join(scope), - 'state': 'abcdefg', - }).replace('+', '%20') - - url = path + '?' + query_str - - return url - - def test_authorize_invalid_parameters(self): + def test_missing_parameters(self): """ If the request fails due to a missing, invalid, or mismatching redirection URI, or if the client identifier is missing or invalid, @@ -43,6 +29,7 @@ class AuthorizationCodeFlowTestCase(TestCase): See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 """ url = reverse('oidc_provider:authorize') + request = self.factory.get(url) response = AuthorizeView.as_view()(request) @@ -50,7 +37,7 @@ class AuthorizationCodeFlowTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(bool(response.content), True) - def test_authorize_invalid_response_type(self): + def test_invalid_response_type(self): """ The OP informs the RP by using the Error Response parameters defined in Section 4.1.2.1 of OAuth 2.0. @@ -58,7 +45,15 @@ class AuthorizationCodeFlowTestCase(TestCase): See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ # Create an authorize request with an unsupported response_type. - url = self._create_authorize_url(response_type='code id_token') + query_str = urllib.urlencode({ + 'client_id': self.client.client_id, + 'response_type': 'something_wrong', + 'redirect_uri': self.client.default_redirect_uri, + 'scope': 'openid email', + 'state': self.state, + }).replace('+', '%20') + + url = reverse('oidc_provider:authorize') + '?' + query_str request = self.factory.get(url) @@ -71,14 +66,22 @@ class AuthorizationCodeFlowTestCase(TestCase): query_exists = 'error=' in response['Location'] self.assertEqual(query_exists, True) - def test_authorize_user_not_logged(self): + def test_codeflow_user_not_logged(self): """ The Authorization Server attempts to Authenticate the End-User by redirecting to the login view. See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates """ - url = self._create_authorize_url(response_type='code') + query_str = urllib.urlencode({ + 'client_id': self.client.client_id, + 'response_type': 'code', + 'redirect_uri': self.client.default_redirect_uri, + 'scope': 'openid email', + 'state': self.state, + }).replace('+', '%20') + + url = reverse('oidc_provider:authorize') + '?' + query_str request = self.factory.get(url) request.user = AnonymousUser() @@ -98,7 +101,7 @@ class AuthorizationCodeFlowTestCase(TestCase): is_next_ok = False self.assertEqual(is_next_ok, True) - def test_authorize_user_consent(self): + def test_codeflow_user_consent_inputs(self): """ Once the End-User is authenticated, the Authorization Server MUST obtain an authorization decision before releasing information to @@ -107,8 +110,17 @@ class AuthorizationCodeFlowTestCase(TestCase): See: http://openid.net/specs/openid-connect-core-1_0.html#Consent """ response_type = 'code' + state = 'openid email' - url = self._create_authorize_url(response_type=response_type) + query_str = urllib.urlencode({ + 'client_id': self.client.client_id, + 'response_type': response_type, + 'redirect_uri': self.client.default_redirect_uri, + 'scope': state, + 'state': self.state, + }).replace('+', '%20') + + url = reverse('oidc_provider:authorize') + '?' + query_str request = self.factory.get(url) # Simulate that the user is logged. @@ -116,9 +128,8 @@ class AuthorizationCodeFlowTestCase(TestCase): response = AuthorizeView.as_view()(request) - # Check if hidden inputs exists in the form, also - # check if their values are valid. - + # Check if hidden inputs exists in the form, + # also if their values are valid. input_html = '' to_check = { @@ -131,3 +142,64 @@ class AuthorizationCodeFlowTestCase(TestCase): is_input_ok = input_html.format(key, value) in response.content self.assertEqual(is_input_ok, True, msg='Hidden input for "'+key+'" fails.') + + def test_codeflow_user_consent_response(self): + """ + First, + if the user denied the consent we must ensure that + the error response parameters are added to the query component + of the Redirection URI. + + Second, + if the user allow the RP then the server MUST return + the parameters defined in Section 4.1.2 of OAuth 2.0 [RFC6749] + by adding them as query parameters to the redirect_uri. + """ + response_type = 'code' + scope = 'openid email' + + url = reverse('oidc_provider:authorize') + + post_data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': response_type, + 'scope': scope, + 'state': self.state, + } + + request = self.factory.post(url, data=post_data) + # Simulate that the user is logged. + request.user = self.user + + response = AuthorizeView.as_view()(request) + + # Because user doesn't allow app, SHOULD exists an error parameter + # in the query. + self.assertEqual('error=' in response['Location'], True) + self.assertEqual('access_denied' in response['Location'], True) + + # Simulate user authorization. + post_data['allow'] = 'Accept' # Should be the value of the button. + + request = self.factory.post(url, data=post_data) + # Simulate that the user is logged. + request.user = self.user + + response = AuthorizeView.as_view()(request) + + # Validate the code returned by the OP. + code = (response['Location'].split('code='))[1].split('&')[0] + try: + code = Code.objects.get(code=code) + if (code.client == self.client) or (code.user == self.user): + is_code_ok = True + else: + is_code_ok = False + except: + is_code_ok = False + self.assertEqual(is_code_ok, True) + + # Check if the state is returned. + state = (response['Location'].split('state='))[1].split('&')[0] + self.assertEqual(state == self.state, True) From 369248426b8e6290fdbbeb7ac3269ddd46ea32e8 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 27 Feb 2015 18:14:46 -0300 Subject: [PATCH 036/125] Update Docs. --- doc.md | 118 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/doc.md b/doc.md index 5f3ce83..fe5fb80 100644 --- a/doc.md +++ b/doc.md @@ -15,10 +15,15 @@ Before getting started there are some important things that you should know: - [Requirements](#requirements) - [Installation](#installation) - [Settings](#settings) + - [SITE_URL](#site_url) + - [LOGIN_URL](#login_url) + - [OIDC_CODE_EXPIRE](#oidc_code_expire) + - [OIDC_EXTRA_SCOPE_CLAIMS](#) + - [OIDC_IDTOKEN_EXPIRE](#) + - [OIDC_TOKEN_EXPIRE](#) - [Users And Clients](#users-and-clients) - [Templates](#templates) - [Server Endpoints](#server-endpoints) -- [Claims And Scopes](#claims-and-scopes) ## Requirements @@ -64,24 +69,64 @@ urlpatterns = patterns('', Add required variables to your project settings. +### SITE_URL +*REQUIRED.* The OP server url. For example `http://localhost:8000`. + +### LOGIN_URL +*REQUIRED.* Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). Default value is `/accounts/login/`. + +### OIDC_CODE_EXPIRE +*OPTIONAL.* Expressed in seconds. Default value is `60*10`. + +### OIDC_EXTRA_SCOPE_CLAIMS +*OPTIONAL.* Used to add extra scopes specific for your app. This class MUST inherit ``AbstractScopeClaims``. + +OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. + +[Here](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) you have the standard scopes defined by the protocol. + +Check out an example: + ```python -# REQUIRED SETTINGS. +from oidc_provider.lib.claims import AbstractScopeClaims -# Your server provider url. -SITE_URL = 'http://localhost:8000' +class MyAppScopeClaims(AbstractScopeClaims): -# Used to log the user in. -# See: https://docs.djangoproject.com/en/1.7/ref/settings/#login-url -LOGIN_URL = '/accounts/login/' + def __init__(self, user, scopes): + # Don't forget this. + super(StandardScopeClaims, self).__init__(user, scopes) -# OPTIONAL SETTINGS. + # Here you can load models that will be used + # in more than one scope for example. + try: + self.some_model = SomeModel.objects.get(user=self.user) + except UserInfo.DoesNotExist: + # Create an empty model object. + self.some_model = SomeModel() -OIDC_CODE_EXPIRE = 60*10 # 10 min. -OIDC_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, -OIDC_IDTOKEN_EXPIRE = 60*10, # 10 min. -OIDC_TOKEN_EXPIRE = 60*60 # 1 hour. + def scope_books(self, user): + + # Here you can search books for this user. + + dic = { + 'books_readed': books_readed_count, + } + + return dic ``` +See how we create our own scopes using the convention: + +``def scope_(self, user):`` + +If a field is empty or ``None`` will be cleaned from the response. + +### OIDC_IDTOKEN_EXPIRE +*OPTIONAL.* Expressed in seconds. Default value is `60*10`. + +### OIDC_TOKEN_EXPIRE +*OPTIONAL.* Expressed in seconds. Default value is `60*60`. + ## Users And Clients User and client creation it's up to you. This is because is out of the scope in the core implementation of OIDC. @@ -179,52 +224,3 @@ POST /openid/userinfo/ HTTP/1.1 Host: localhost:8000 Authorization: Bearer [ACCESS_TOKEN] ``` - -## Claims And Scopes - -OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. - -Here you have the standard scopes defined by the protocol. -http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims - -If you need to add extra scopes specific for your app you can add them using the ``DOP_EXTRA_SCOPE_CLAIMS`` settings variable. -This class MUST inherit ``AbstractScopeClaims``. - -Check out an example: - -```python -from openid_provider.lib.claims import AbstractScopeClaims - -class MyAppScopeClaims(AbstractScopeClaims): - - def __init__(self, user, scopes): - # Don't forget this. - super(StandardScopeClaims, self).__init__(user, scopes) - - # Here you can load models that will be used - # in more than one scope for example. - try: - self.some_model = SomeModel.objects.get(user=self.user) - except UserInfo.DoesNotExist: - # Create an empty model object. - self.some_model = SomeModel() - - def scope_books(self, user): - - # Here you can search books for this user. - # Remember that you have "self.some_model" also. - - dic = { - 'books_readed': books_readed_count, - } - - return dic -``` - -See how we create our own scopes using the convention: - -``def scope_(self, user):`` - -If a field is empty or ``None`` will be cleaned from the response. - -**Don't forget to add your class into your app settings.** From c8a4eb90f243d3636c37acaa0a3ca601031c5339 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 27 Feb 2015 18:17:20 -0300 Subject: [PATCH 037/125] Update Docs. --- doc.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc.md b/doc.md index fe5fb80..21ed103 100644 --- a/doc.md +++ b/doc.md @@ -18,9 +18,9 @@ Before getting started there are some important things that you should know: - [SITE_URL](#site_url) - [LOGIN_URL](#login_url) - [OIDC_CODE_EXPIRE](#oidc_code_expire) - - [OIDC_EXTRA_SCOPE_CLAIMS](#) - - [OIDC_IDTOKEN_EXPIRE](#) - - [OIDC_TOKEN_EXPIRE](#) + - [OIDC_EXTRA_SCOPE_CLAIMS](#oidc_extra_scope_claims) + - [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire) + - [OIDC_TOKEN_EXPIRE](#oidc_token_expire) - [Users And Clients](#users-and-clients) - [Templates](#templates) - [Server Endpoints](#server-endpoints) From 8c98c87fc92edc643e2f248841733417f5bd6e0e Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 27 Feb 2015 18:23:06 -0300 Subject: [PATCH 038/125] Update Docs. --- doc.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc.md b/doc.md index 21ed103..32c61d4 100644 --- a/doc.md +++ b/doc.md @@ -69,16 +69,16 @@ urlpatterns = patterns('', Add required variables to your project settings. -### SITE_URL +##### SITE_URL *REQUIRED.* The OP server url. For example `http://localhost:8000`. -### LOGIN_URL -*REQUIRED.* Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). Default value is `/accounts/login/`. +##### LOGIN_URL +*REQUIRED.* Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). Default is `/accounts/login/`. -### OIDC_CODE_EXPIRE -*OPTIONAL.* Expressed in seconds. Default value is `60*10`. +##### OIDC_CODE_EXPIRE +*OPTIONAL.* Expressed in seconds. Default is `60*10`. -### OIDC_EXTRA_SCOPE_CLAIMS +##### OIDC_EXTRA_SCOPE_CLAIMS *OPTIONAL.* Used to add extra scopes specific for your app. This class MUST inherit ``AbstractScopeClaims``. OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. @@ -121,11 +121,11 @@ See how we create our own scopes using the convention: If a field is empty or ``None`` will be cleaned from the response. -### OIDC_IDTOKEN_EXPIRE -*OPTIONAL.* Expressed in seconds. Default value is `60*10`. +##### OIDC_IDTOKEN_EXPIRE +*OPTIONAL.* Expressed in seconds. Default is `60*10`. -### OIDC_TOKEN_EXPIRE -*OPTIONAL.* Expressed in seconds. Default value is `60*60`. +##### OIDC_TOKEN_EXPIRE +*OPTIONAL.* Token object expiration after been created. Expressed in seconds. Default is `60*60`. ## Users And Clients From 377a4d37e9477dd61ea451a7157ad4d8e9e1a1af Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Sun, 1 Mar 2015 20:21:37 -0300 Subject: [PATCH 039/125] Update Docs. --- doc.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc.md b/doc.md index 32c61d4..dbb9731 100644 --- a/doc.md +++ b/doc.md @@ -7,7 +7,7 @@ Django OIDC Provider can help you providing out of the box all the endpoints, da Before getting started there are some important things that you should know: * Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. * Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. -* This cover authorization_code flow and implicit flow, NO support for hybrid flow at this moment. +* This cover `Authorization Code` flow and `Implicit` flow, NO support for `Hybrid` flow at this moment. * Only support for requesting Claims using Scope Values. # Table Of Contents @@ -70,16 +70,16 @@ urlpatterns = patterns('', Add required variables to your project settings. ##### SITE_URL -*REQUIRED.* The OP server url. For example `http://localhost:8000`. +REQUIRED. The OP server url. For example `http://localhost:8000`. ##### LOGIN_URL -*REQUIRED.* Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). Default is `/accounts/login/`. +REQUIRED. Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). Default is `/accounts/login/`. ##### OIDC_CODE_EXPIRE -*OPTIONAL.* Expressed in seconds. Default is `60*10`. +OPTIONAL. Expressed in seconds. Default is `60*10`. ##### OIDC_EXTRA_SCOPE_CLAIMS -*OPTIONAL.* Used to add extra scopes specific for your app. This class MUST inherit ``AbstractScopeClaims``. +OPTIONAL. Used to add extra scopes specific for your app. This class MUST inherit ``AbstractScopeClaims``. OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. @@ -122,10 +122,10 @@ See how we create our own scopes using the convention: If a field is empty or ``None`` will be cleaned from the response. ##### OIDC_IDTOKEN_EXPIRE -*OPTIONAL.* Expressed in seconds. Default is `60*10`. +OPTIONAL. Expressed in seconds. Default is `60*10`. ##### OIDC_TOKEN_EXPIRE -*OPTIONAL.* Token object expiration after been created. Expressed in seconds. Default is `60*60`. +OPTIONAL. Token object expiration after been created. Expressed in seconds. Default is `60*60`. ## Users And Clients From 31905999670901af720e02bcefd27310c1fd67f4 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 2 Mar 2015 17:37:54 -0300 Subject: [PATCH 040/125] Add custom SUB generator for ID TOKEN. --- oidc_provider/lib/endpoints/token.py | 17 +++++++--- oidc_provider/lib/utils/token.py | 18 ++++++----- oidc_provider/settings.py | 47 ++++++++++++++++------------ 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 87f73f1..37eaf44 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -1,10 +1,12 @@ +import urllib + from django.http import JsonResponse + from oidc_provider.lib.errors import * from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.token import * from oidc_provider.models import * from oidc_provider import settings -import urllib class TokenEndpoint(object): @@ -55,10 +57,15 @@ class TokenEndpoint(object): def create_response_dic(self): - id_token_dic = create_id_token_dic( - self.code.user, - settings.get('SITE_URL'), - self.client.client_id) + sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( + user=self.code.user, + client=self.client) + + id_token_dic = create_id_token( + iss=settings.get('SITE_URL'), + sub=sub, + aud=self.client.client_id, + auth_time=self.code.user.last_login) token = create_token( user=self.code.user, diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index d24ac7c..0dcd019 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -1,13 +1,15 @@ from datetime import timedelta -from django.utils import timezone -from oidc_provider.models import * -from oidc_provider import settings -import jwt import time import uuid +from django.utils import timezone +import jwt -def create_id_token_dic(user, iss, aud): +from oidc_provider.models import * +from oidc_provider import settings + + +def create_id_token(iss, sub, aud, auth_time): """ Receives a user object, iss (issuer) and aud (audience). Then creates the id_token dic. @@ -22,11 +24,11 @@ def create_id_token_dic(user, iss, aud): # Convert datetimes into timestamps. iat_time = time.mktime(now.timetuple()) exp_time = time.mktime((now + timedelta(seconds=expires_in)).timetuple()) - user_auth_time = time.mktime(user.last_login.timetuple()) + user_auth_time = time.mktime(auth_time.timetuple()) dic = { 'iss': iss, - 'sub': user.id, + 'sub': sub, 'aud': aud, 'exp': exp_time, 'iat': iat_time, @@ -65,4 +67,4 @@ def create_token(user, client, id_token_dic, scope): seconds=settings.get('OIDC_TOKEN_EXPIRE')) token.scope = scope - return token + return token \ No newline at end of file diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index f557d39..a661890 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -1,30 +1,37 @@ from django.conf import settings + from oidc_provider.lib.claims import AbstractScopeClaims +def default_sub_generator(user, client): + + return user.id + + # Here goes all the package default settings. - default_settings = { - # Required. - 'LOGIN_URL': None, - 'SITE_URL': None, + # Required. + 'LOGIN_URL': None, + 'SITE_URL': None, - # Optional. - 'OIDC_CODE_EXPIRE': 60*10, - 'OIDC_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, - 'OIDC_IDTOKEN_EXPIRE': 60*10, - 'OIDC_TOKEN_EXPIRE': 60*60, + # Optional. + 'OIDC_CODE_EXPIRE': 60*10, + 'OIDC_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, + 'OIDC_IDTOKEN_EXPIRE': 60*10, + 'OIDC_IDTOKEN_SUB_GENERATOR': default_sub_generator, + 'OIDC_TOKEN_EXPIRE': 60*60, } -def get(name): - ''' - Helper function to use inside the package. - ''' - try: - value = default_settings[name] - value = getattr(settings, name) - except AttributeError: - if value == None: - raise Exception('You must set ' + name + ' in your settings.') - return value +def get(name): + ''' + Helper function to use inside the package. + ''' + try: + value = default_settings[name] + value = getattr(settings, name) + except AttributeError: + if value == None: + raise Exception('You must set ' + name + ' in your settings.') + + return value \ No newline at end of file From f60ac01d97f67793fb5877c5948e7a8587c7097c Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 2 Mar 2015 17:46:55 -0300 Subject: [PATCH 041/125] Add SUB_GENERATOR setting to docs. --- doc.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/doc.md b/doc.md index dbb9731..546bfa1 100644 --- a/doc.md +++ b/doc.md @@ -20,6 +20,7 @@ Before getting started there are some important things that you should know: - [OIDC_CODE_EXPIRE](#oidc_code_expire) - [OIDC_EXTRA_SCOPE_CLAIMS](#oidc_extra_scope_claims) - [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire) + - [OIDC_IDTOKEN_SUB_GENERATOR](#oidc_idtoken_sub_generator) - [OIDC_TOKEN_EXPIRE](#oidc_token_expire) - [Users And Clients](#users-and-clients) - [Templates](#templates) @@ -73,10 +74,12 @@ Add required variables to your project settings. REQUIRED. The OP server url. For example `http://localhost:8000`. ##### LOGIN_URL -REQUIRED. Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). Default is `/accounts/login/`. +REQUIRED. Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). +Default is `/accounts/login/`. ##### OIDC_CODE_EXPIRE -OPTIONAL. Expressed in seconds. Default is `60*10`. +OPTIONAL. Expressed in seconds. +Default is `60*10`. ##### OIDC_EXTRA_SCOPE_CLAIMS OPTIONAL. Used to add extra scopes specific for your app. This class MUST inherit ``AbstractScopeClaims``. @@ -122,10 +125,21 @@ See how we create our own scopes using the convention: If a field is empty or ``None`` will be cleaned from the response. ##### OIDC_IDTOKEN_EXPIRE -OPTIONAL. Expressed in seconds. Default is `60*10`. +OPTIONAL. Expressed in seconds. +Default is `60*10`. + +##### OIDC_IDTOKEN_SUB_GENERATOR +OPTIONAL. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. +Is a function that receives both `user` and `client` objects. Default is: +```python +def default_sub_generator(user, client): + + return user.id +``` ##### OIDC_TOKEN_EXPIRE -OPTIONAL. Token object expiration after been created. Expressed in seconds. Default is `60*60`. +OPTIONAL. Token object expiration after been created. Expressed in seconds. +Default is `60*60`. ## Users And Clients From 604f94e15b3d01484f5041b1e0a2754d14044cb7 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Mon, 2 Mar 2015 17:51:35 -0300 Subject: [PATCH 042/125] Update Docs. --- doc.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc.md b/doc.md index 546bfa1..5c5e55b 100644 --- a/doc.md +++ b/doc.md @@ -75,10 +75,12 @@ REQUIRED. The OP server url. For example `http://localhost:8000`. ##### LOGIN_URL REQUIRED. Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url). + Default is `/accounts/login/`. ##### OIDC_CODE_EXPIRE OPTIONAL. Expressed in seconds. + Default is `60*10`. ##### OIDC_EXTRA_SCOPE_CLAIMS @@ -126,11 +128,15 @@ If a field is empty or ``None`` will be cleaned from the response. ##### OIDC_IDTOKEN_EXPIRE OPTIONAL. Expressed in seconds. + Default is `60*10`. ##### OIDC_IDTOKEN_SUB_GENERATOR OPTIONAL. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. -Is a function that receives both `user` and `client` objects. Default is: + +Is just a function that receives both `user` and `client` objects, returns a string. + +Default is: ```python def default_sub_generator(user, client): @@ -139,6 +145,7 @@ def default_sub_generator(user, client): ##### OIDC_TOKEN_EXPIRE OPTIONAL. Token object expiration after been created. Expressed in seconds. + Default is `60*60`. ## Users And Clients From 84ad3cae27b0e25f1bc792eda5dd71fadb8be3c7 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Tue, 3 Mar 2015 12:30:11 -0300 Subject: [PATCH 043/125] Update Docs. --- doc.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc.md b/doc.md index 5c5e55b..5f0b018 100644 --- a/doc.md +++ b/doc.md @@ -1,3 +1,5 @@ +![OpenID Connect](http://wiki.openid.net/f/openid-logo-wordmark.png) + # Welcome to the Docs! Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. @@ -90,7 +92,7 @@ OpenID Connect Clients will use scope values to specify what access privileges a [Here](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) you have the standard scopes defined by the protocol. -Check out an example: +Check out an example of how to implement it: ```python from oidc_provider.lib.claims import AbstractScopeClaims From 964d649d3d6db5032186dc3ebb6cf4f619ea0057 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 3 Mar 2015 13:39:32 -0300 Subject: [PATCH 044/125] Remove client from sub generator. --- doc.md | 2 +- oidc_provider/lib/endpoints/token.py | 3 +-- oidc_provider/settings.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc.md b/doc.md index 546bfa1..464981a 100644 --- a/doc.md +++ b/doc.md @@ -132,7 +132,7 @@ Default is `60*10`. OPTIONAL. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. Is a function that receives both `user` and `client` objects. Default is: ```python -def default_sub_generator(user, client): +def default_sub_generator(user): return user.id ``` diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 37eaf44..d2c3582 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -58,8 +58,7 @@ class TokenEndpoint(object): def create_response_dic(self): sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( - user=self.code.user, - client=self.client) + user=self.code.user) id_token_dic = create_id_token( iss=settings.get('SITE_URL'), diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index a661890..d2d1209 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -3,7 +3,7 @@ from django.conf import settings from oidc_provider.lib.claims import AbstractScopeClaims -def default_sub_generator(user, client): +def default_sub_generator(user): return user.id From 3ad316cdca1a2d0989a05e7ce35566fc530706e4 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 4 Mar 2015 16:24:41 -0300 Subject: [PATCH 045/125] Add Provider Configuration Information endpoint. --- oidc_provider/lib/endpoints/authorize.py | 4 ++- oidc_provider/lib/endpoints/discovery.py | 31 ++++++++++++++++++++++++ oidc_provider/lib/endpoints/token.py | 3 ++- oidc_provider/lib/endpoints/userinfo.py | 4 ++- oidc_provider/lib/utils/common.py | 16 ++++++++++++ oidc_provider/urls.py | 2 ++ oidc_provider/views.py | 15 +++++++++++- 7 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 oidc_provider/lib/endpoints/discovery.py create mode 100644 oidc_provider/lib/utils/common.py diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index b6ca2fe..362dd56 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -1,11 +1,13 @@ from datetime import timedelta +import uuid + from django.utils import timezone + from oidc_provider.lib.errors import * from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.token import * from oidc_provider.models import * from oidc_provider import settings -import uuid class AuthorizeEndpoint(object): diff --git a/oidc_provider/lib/endpoints/discovery.py b/oidc_provider/lib/endpoints/discovery.py new file mode 100644 index 0000000..945695f --- /dev/null +++ b/oidc_provider/lib/endpoints/discovery.py @@ -0,0 +1,31 @@ +from django.core.urlresolvers import reverse + +from oidc_provider import settings +from oidc_provider.lib.utils.common import get_issuer + + +class ProviderInfoEndpoint(object): + + @classmethod + def create_response_dic(cls): + dic = {} + + dic['issuer'] = get_issuer() + + SITE_URL = settings.get('SITE_URL') + + dic['authorization_endpoint'] = SITE_URL + reverse('oidc_provider:authorize') + dic['token_endpoint'] = SITE_URL + reverse('oidc_provider:token') + dic['userinfo_endpoint'] = SITE_URL + reverse('oidc_provider:userinfo') + + from oidc_provider.models import Client + types_supported = [x[0] for x in Client.RESPONSE_TYPE_CHOICES] + dic['response_types_supported'] = types_supported + + # TODO: + #dic['jwks_uri'] = None + + # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes + dic['subject_types_supported'] = ['public'] + + return dic \ No newline at end of file diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index d2c3582..47437ae 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -3,6 +3,7 @@ import urllib from django.http import JsonResponse from oidc_provider.lib.errors import * +from oidc_provider.lib.utils.common import get_issuer from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.token import * from oidc_provider.models import * @@ -61,7 +62,7 @@ class TokenEndpoint(object): user=self.code.user) id_token_dic = create_id_token( - iss=settings.get('SITE_URL'), + iss=get_issuer(), sub=sub, aud=self.client.client_id, auth_time=self.code.user.last_login) diff --git a/oidc_provider/lib/endpoints/userinfo.py b/oidc_provider/lib/endpoints/userinfo.py index 163dfd5..3a7a48c 100644 --- a/oidc_provider/lib/endpoints/userinfo.py +++ b/oidc_provider/lib/endpoints/userinfo.py @@ -1,11 +1,13 @@ +import re + from django.http import HttpResponse from django.http import JsonResponse + from oidc_provider.lib.errors import * from oidc_provider.lib.claims import * from oidc_provider.lib.utils.params import * from oidc_provider.models import * from oidc_provider import settings -import re class UserInfoEndpoint(object): diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py new file mode 100644 index 0000000..7f0a626 --- /dev/null +++ b/oidc_provider/lib/utils/common.py @@ -0,0 +1,16 @@ +from django.core.urlresolvers import reverse + +from oidc_provider import settings + + +def get_issuer(): + """ + Construct the issuer full url. Basically is the site url with some path + appended. + """ + site_url = settings.get('SITE_URL') + path = reverse('oidc_provider:provider_info') \ + .split('/.well-known/openid-configuration/')[0] + issuer = site_url + path + + return issuer diff --git a/oidc_provider/urls.py b/oidc_provider/urls.py index 54c4f62..1667b48 100644 --- a/oidc_provider/urls.py +++ b/oidc_provider/urls.py @@ -9,4 +9,6 @@ urlpatterns = patterns('', url(r'^token/$', csrf_exempt(TokenView.as_view()), name='token'), url(r'^userinfo/$', csrf_exempt(userinfo), name='userinfo'), + url(r'^\.well-known/openid-configuration/$', ProviderInfoView.as_view(), name='provider_info'), + ) \ No newline at end of file diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 7b4a693..928b838 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -4,10 +4,12 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.views.decorators.http import require_http_methods from django.views.generic import View -from oidc_provider.lib.errors import * + from oidc_provider.lib.endpoints.authorize import * +from oidc_provider.lib.endpoints.discovery import * from oidc_provider.lib.endpoints.token import * from oidc_provider.lib.endpoints.userinfo import * +from oidc_provider.lib.errors import * class AuthorizeView(View): @@ -75,6 +77,7 @@ class AuthorizeView(View): return HttpResponseRedirect(uri) + class TokenView(View): def post(self, request, *args, **kwargs): @@ -91,6 +94,7 @@ class TokenView(View): except (TokenError) as error: return TokenEndpoint.response(error.create_dict(), status=400) + @require_http_methods(['GET', 'POST']) def userinfo(request): @@ -108,3 +112,12 @@ def userinfo(request): error.code, error.description, error.status) + + +class ProviderInfoView(View): + + def get(self, request, *args, **kwargs): + + dic = ProviderInfoEndpoint.create_response_dic() + + return JsonResponse(dic) \ No newline at end of file From a97ef2b5cfe2d12bf3eda359e3dad1a33cadf8ee Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 4 Mar 2015 17:17:37 -0300 Subject: [PATCH 046/125] Set default settings using class attr. --- oidc_provider/settings.py | 67 +++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index d2d1209..47669c9 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -1,26 +1,61 @@ from django.conf import settings -from oidc_provider.lib.claims import AbstractScopeClaims +class DefaultSettings(object): -def default_sub_generator(user): + @property + def LOGIN_URL(self): + """ + REQUIRED. + """ + return None - return user.id + @property + def SITE_URL(self): + """ + REQUIRED. + """ + return None + @property + def OIDC_CODE_EXPIRE(self): + """ + OPTIONAL. + """ + return 60*10 -# Here goes all the package default settings. -default_settings = { - # Required. - 'LOGIN_URL': None, - 'SITE_URL': None, + @property + def OIDC_EXTRA_SCOPE_CLAIMS(self): + """ + OPTIONAL. + """ + from oidc_provider.lib.claims import AbstractScopeClaims - # Optional. - 'OIDC_CODE_EXPIRE': 60*10, - 'OIDC_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, - 'OIDC_IDTOKEN_EXPIRE': 60*10, - 'OIDC_IDTOKEN_SUB_GENERATOR': default_sub_generator, - 'OIDC_TOKEN_EXPIRE': 60*60, -} + return AbstractScopeClaims + + @property + def OIDC_IDTOKEN_EXPIRE(self): + """ + OPTIONAL. + """ + return 60*10 + + @property + def OIDC_IDTOKEN_SUB_GENERATOR(self): + """ + OPTIONAL. + """ + def default_sub_generator(user): + return user.id + + return default_sub_generator + + @property + def OIDC_TOKEN_EXPIRE(self): + """ + OPTIONAL. + """ + return 60*60 def get(name): @@ -28,7 +63,7 @@ def get(name): Helper function to use inside the package. ''' try: - value = default_settings[name] + value = getattr(DefaultSettings(), name) value = getattr(settings, name) except AttributeError: if value == None: From 2e9cd8824cd107c5ab72d116a1a2e7419a1c7848 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 5 Mar 2015 12:42:49 -0300 Subject: [PATCH 047/125] Update Docs. --- doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.md b/doc.md index b01d968..d837e61 100644 --- a/doc.md +++ b/doc.md @@ -136,7 +136,7 @@ Default is `60*10`. ##### OIDC_IDTOKEN_SUB_GENERATOR OPTIONAL. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. -Is just a function that receives both `user` and `client` objects, returns a string. +Is just a function that receives a `user` object. Returns a unique string for the given user. Default is: ```python From df790c7e5b947cea58135d9f1d013b57c33112fa Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 5 Mar 2015 13:12:18 -0300 Subject: [PATCH 048/125] Create CHANGELOG. --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..89e6e6c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# CHANGELOG + +All notable changes to this project will be documented in this file. + +## [Unreleased] +### Added +- Setting for custom SUB generator in ID TOKEN. + +## [0.0.0] - 2015-02-26 +### Added +- Initial release. From fe5812a9cf9afad7d2debb76dca3d95144024d02 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 5 Mar 2015 14:30:29 -0300 Subject: [PATCH 049/125] Update CHANGELOG. --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e6e6c..7b2d527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ All notable changes to this project will be documented in this file. -## [Unreleased] -### Added +### [Unreleased] +##### Added - Setting for custom SUB generator in ID TOKEN. -## [0.0.0] - 2015-02-26 -### Added +### [0.0.0] - 2015-02-26 +##### Added - Initial release. From 2a7cf82cd43a749c8910f344142f45ed693bb9c6 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 5 Mar 2015 15:51:05 -0300 Subject: [PATCH 050/125] Add changelog to README. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index fde0bbc..9488dd3 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,8 @@ Django OIDC Provider can help you providing out of the box all the endpoints, da Read docs for more info. https://github.com/juanifioren/django-oidc-provider/blob/master/doc.md +See changelog here. https://github.com/juanifioren/django-oidc-provider/blob/master/CHANGELOG.md + ************* Running tests ************* From 8c4e780f9cf04cee3bf7d25267aaadef6cea342c Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 5 Mar 2015 16:49:10 -0300 Subject: [PATCH 051/125] Update CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2d527..a6cb7df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ### [Unreleased] ##### Added +- Provider Configuration Information endpoint. - Setting for custom SUB generator in ID TOKEN. ### [0.0.0] - 2015-02-26 From d01c5b41eb03b9548d4d8675110d8610eeecee44 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 5 Mar 2015 16:49:27 -0300 Subject: [PATCH 052/125] Update CHANGELOG. --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6cb7df..b8dceef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,5 +8,3 @@ All notable changes to this project will be documented in this file. - Setting for custom SUB generator in ID TOKEN. ### [0.0.0] - 2015-02-26 -##### Added -- Initial release. From e3b5af54903ff2b1f150706068ea58d0b6077949 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 6 Mar 2015 12:54:27 -0300 Subject: [PATCH 053/125] Add setup to extra scope claims. --- doc.md | 7 +++---- oidc_provider/lib/claims.py | 9 ++++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc.md b/doc.md index d837e61..795082f 100644 --- a/doc.md +++ b/doc.md @@ -99,12 +99,11 @@ from oidc_provider.lib.claims import AbstractScopeClaims class MyAppScopeClaims(AbstractScopeClaims): - def __init__(self, user, scopes): - # Don't forget this. - super(StandardScopeClaims, self).__init__(user, scopes) - + def setup(self): # Here you can load models that will be used # in more than one scope for example. + # print self.user + # print self.scopes try: self.some_model = SomeModel.objects.get(user=self.user) except UserInfo.DoesNotExist: diff --git a/oidc_provider/lib/claims.py b/oidc_provider/lib/claims.py index 3c59bdc..e9d855a 100644 --- a/oidc_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -8,6 +8,11 @@ class AbstractScopeClaims(object): self.user = user self.scopes = scopes + self.setup() + + def setup(self): + pass + def create_response_dic(self): """ Generate the dic that will be jsonify. Checking scopes given vs @@ -61,9 +66,7 @@ class StandardScopeClaims(AbstractScopeClaims): See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims """ - def __init__(self, user, scopes): - super(StandardScopeClaims, self).__init__(user, scopes) - + def setup(self): try: self.userinfo = UserInfo.objects.get(user=self.user) except UserInfo.DoesNotExist: From 2c76393c0985d6e33124e1a7c13dafb368ff1d6e Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 6 Mar 2015 12:55:50 -0300 Subject: [PATCH 054/125] Move class instantiation in settings. --- oidc_provider/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 47669c9..80959c5 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -57,13 +57,14 @@ class DefaultSettings(object): """ return 60*60 +default_settings = DefaultSettings() def get(name): ''' Helper function to use inside the package. ''' try: - value = getattr(DefaultSettings(), name) + value = getattr(default_settings, name) value = getattr(settings, name) except AttributeError: if value == None: From c9c5982c35ebed86982bd703bd0310857fbe917e Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 6 Mar 2015 12:56:35 -0300 Subject: [PATCH 055/125] Add provider info test. Add some msg to tests. --- .../tests/test_authorize_endpoint.py | 12 ++++++--- .../tests/test_provider_info_endpoint.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 oidc_provider/tests/test_provider_info_endpoint.py diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 9122235..e661814 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -176,8 +176,10 @@ class AuthorizationCodeFlowTestCase(TestCase): # Because user doesn't allow app, SHOULD exists an error parameter # in the query. - self.assertEqual('error=' in response['Location'], True) - self.assertEqual('access_denied' in response['Location'], True) + self.assertEqual('error=' in response['Location'], True, + msg='error param is missing.') + self.assertEqual('access_denied' in response['Location'], True, + msg='access_denied param is missing.') # Simulate user authorization. post_data['allow'] = 'Accept' # Should be the value of the button. @@ -198,8 +200,10 @@ class AuthorizationCodeFlowTestCase(TestCase): is_code_ok = False except: is_code_ok = False - self.assertEqual(is_code_ok, True) + 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] - self.assertEqual(state == self.state, True) + self.assertEqual(state == self.state, True, + msg='State change or is missing.') diff --git a/oidc_provider/tests/test_provider_info_endpoint.py b/oidc_provider/tests/test_provider_info_endpoint.py new file mode 100644 index 0000000..1b205bc --- /dev/null +++ b/oidc_provider/tests/test_provider_info_endpoint.py @@ -0,0 +1,26 @@ +from django.core.urlresolvers import reverse +from django.test import RequestFactory +from django.test import TestCase + +from oidc_provider.views import * + + +class ProviderInfoTestCase(TestCase): + + def setUp(self): + self.factory = RequestFactory() + + def test_response(self): + """ + See if the endpoint is returning the corresponding + server information by checking status, content type, etc. + """ + url = reverse('oidc_provider:provider_info') + + request = self.factory.get(url) + + response = ProviderInfoView.as_view()(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'] == 'application/json', True) + self.assertEqual(bool(response.content), True) \ No newline at end of file From be29ed34ffcf973e2ebd5e55fd5895f3af0d8657 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 6 Mar 2015 14:13:56 -0300 Subject: [PATCH 056/125] Update CHANGELOG. --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8dceef..94cdafa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] ##### Added - Provider Configuration Information endpoint. -- Setting for custom SUB generator in ID TOKEN. +- Setting OIDC_IDTOKEN_SUB_GENERATOR. +##### Changed +- Now use setup in OIDC_EXTRA_SCOPE_CLAIMS setting. ### [0.0.0] - 2015-02-26 From 46daf6612cd7dd27be2eec3110648250f603c59d Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 6 Mar 2015 14:15:54 -0300 Subject: [PATCH 057/125] Fix CHANGELOG. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cdafa..64c64b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ All notable changes to this project will be documented in this file. ### [Unreleased] + ##### Added - Provider Configuration Information endpoint. - Setting OIDC_IDTOKEN_SUB_GENERATOR. + ##### Changed - Now use setup in OIDC_EXTRA_SCOPE_CLAIMS setting. From 5ee96336583d2bb231796a0e99112b3034668afd Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 6 Mar 2015 14:15:22 -0300 Subject: [PATCH 058/125] Rename docs file. --- doc.md => DOC.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc.md => DOC.md (100%) diff --git a/doc.md b/DOC.md similarity index 100% rename from doc.md rename to DOC.md From 01fc8ee43d6802ce43dfa3fd538e023dd17cfc7e Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 6 Mar 2015 17:50:10 -0300 Subject: [PATCH 059/125] Fix README. --- README.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9488dd3..915088b 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,13 @@ Django OIDC Provider Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. -Read docs for more info. https://github.com/juanifioren/django-oidc-provider/blob/master/doc.md +Read docs for more info. -See changelog here. https://github.com/juanifioren/django-oidc-provider/blob/master/CHANGELOG.md +https://github.com/juanifioren/django-oidc-provider/blob/master/DOC.md + +See changelog here. + +https://github.com/juanifioren/django-oidc-provider/blob/master/CHANGELOG.md ************* Running tests From dda38fd4518f2396dee19ebf6941d8d768d32c95 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 11 Mar 2015 14:36:52 -0300 Subject: [PATCH 060/125] Fix in authorize endpoint tests. --- oidc_provider/tests/test_authorize_endpoint.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index e661814..e06bc16 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -13,6 +13,10 @@ from oidc_provider.views import * class AuthorizationCodeFlowTestCase(TestCase): + """ + An Authentication Request is an OAuth 2.0 Authorization Request that + requests that the End-User be authenticated by the Authorization Server. + """ def setUp(self): self.factory = RequestFactory() @@ -194,10 +198,8 @@ class AuthorizationCodeFlowTestCase(TestCase): code = (response['Location'].split('code='))[1].split('&')[0] try: code = Code.objects.get(code=code) - if (code.client == self.client) or (code.user == self.user): - is_code_ok = True - else: - is_code_ok = False + is_code_ok = (code.client == self.client) and \ + (code.user == self.user) except: is_code_ok = False self.assertEqual(is_code_ok, True, @@ -207,3 +209,10 @@ class AuthorizationCodeFlowTestCase(TestCase): state = (response['Location'].split('state='))[1].split('&')[0] self.assertEqual(state == self.state, True, msg='State change or is missing.') + + +class AuthorizationImplicitFlowTestCase(TestCase): + """ + TODO. + """ + pass \ No newline at end of file From 7e690f4e689cf1d1db5457be70ee78c5bee65297 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 12 Mar 2015 12:40:36 -0300 Subject: [PATCH 061/125] Move Grant Code creation logic into a functon. --- oidc_provider/lib/endpoints/authorize.py | 12 +++++------- oidc_provider/lib/utils/token.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 362dd56..d6682f5 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -95,13 +95,11 @@ class AuthorizeEndpoint(object): if self.grant_type == 'authorization_code': - code = Code() - code.user = self.request.user - code.client = self.client - code.code = uuid.uuid4().hex - code.expires_at = timezone.now() + timedelta( - seconds=settings.get('OIDC_CODE_EXPIRE')) - code.scope = self.params.scope + code = create_code( + user=self.request.user, + client=self.client, + scope=self.params.scope) + code.save() # Create the response uri. diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 0dcd019..74d04cb 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -67,4 +67,21 @@ def create_token(user, client, id_token_dic, scope): seconds=settings.get('OIDC_TOKEN_EXPIRE')) token.scope = scope - return token \ No newline at end of file + return token + + +def create_code(user, client, scope): + """ + Create and populate a Code object. + + Return a Code object. + """ + code = Code() + code.user = user + code.client = client + code.code = uuid.uuid4().hex + code.expires_at = timezone.now() + timedelta( + seconds=settings.get('OIDC_CODE_EXPIRE')) + code.scope = scope + + return code \ No newline at end of file From 46f57ae7f2e88e9f68ddf3a5d037ef7bdddefd74 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 12 Mar 2015 12:42:52 -0300 Subject: [PATCH 062/125] Unnecessary assignment in test_authorize_endpoint. --- oidc_provider/tests/test_authorize_endpoint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index e06bc16..4946f0a 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -1,4 +1,5 @@ import urllib +import uuid from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import AnonymousUser @@ -160,7 +161,6 @@ class AuthorizationCodeFlowTestCase(TestCase): by adding them as query parameters to the redirect_uri. """ response_type = 'code' - scope = 'openid email' url = reverse('oidc_provider:authorize') @@ -168,7 +168,7 @@ class AuthorizationCodeFlowTestCase(TestCase): 'client_id': self.client.client_id, 'redirect_uri': self.client.default_redirect_uri, 'response_type': response_type, - 'scope': scope, + 'scope': 'openid email', 'state': self.state, } From 1ec93d480f373fab40d3b9cd385277867aef9c03 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 12 Mar 2015 12:43:21 -0300 Subject: [PATCH 063/125] Add test_token_endpoint. --- oidc_provider/tests/test_token_endpoint.py | 121 +++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 oidc_provider/tests/test_token_endpoint.py diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py new file mode 100644 index 0000000..6ea419d --- /dev/null +++ b/oidc_provider/tests/test_token_endpoint.py @@ -0,0 +1,121 @@ +import json +from urllib import urlencode +import uuid + +from django.test import RequestFactory +from django.test import TestCase + +from oidc_provider.lib.utils.token import * +from oidc_provider.tests.utils import * +from oidc_provider.views import * + + +class TokenTestCase(TestCase): + """ + To obtain an Access Token and an ID Token, the RP Client sends a + Token Request to the Token Endpoint to obtain a Token Response + when using the Authorization Code Flow. + """ + + def setUp(self): + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='code') + self.state = uuid.uuid4().hex + + def _post_request(self, post_data): + """ + Makes a request to the token endpoint by sending the + `post_data` parameters using the 'application/x-www-form-urlencoded' + format. + """ + url = reverse('oidc_provider:token') + + request = self.factory.post(url, + data=urlencode(post_data), + content_type='application/x-www-form-urlencoded') + + response = TokenView.as_view()(request) + + return response + + def _create_code(self): + """ + Generate a valid grant code. + """ + code = create_code( + user=self.user, + client=self.client, + scope=['openid', 'email']) + code.save() + + return code + + def test_request_methods(self): + """ + Client sends an HTTP POST request to the Token Endpoint. Other request + methods MUST NOT be allowed. + """ + url = reverse('oidc_provider:token') + + requests = [ + self.factory.get(url), + self.factory.put(url), + self.factory.delete(url), + ] + + 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.') + + 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.') + + def test_client_authentication(self): + """ + The authorization server support including the + client credentials in the request-body using the `client_id` and + `client_secret`parameters. + + See: http://tools.ietf.org/html/rfc6749#section-2.3.1 + """ + code = self._create_code() + + # Test a valid request to the token endpoint. + post_data = { + 'client_id': self.client.client_id, + 'client_secret': self.client.client_secret, + 'redirect_uri': self.client.default_redirect_uri, + 'grant_type': 'authorization_code', + 'code': code.code, + 'state': self.state, + } + response = self._post_request(post_data) + response_dic = json.loads(response.content) + + self.assertEqual('access_token' in response_dic, True, + msg='"access_token" key is missing in response.') + self.assertEqual('error' in response_dic, False, + msg='"error" key should not exists in response.') + + # Now, test with an invalid client_id. + invalid_data = post_data.copy() + invalid_data['client_id'] = self.client.client_id * 2 # Fake id. + + # Create another grant code. + code = self._create_code() + invalid_data['code'] = code.code + + response = self._post_request(invalid_data) + response_dic = json.loads(response.content) + + self.assertEqual('error' in response_dic, True, + msg='"error" key should exists in response.') + self.assertEqual(response_dic.get('error') == 'invalid_client', True, + msg='"error" key value should be "invalid_client".') From 5c415c8da7b2237101a2dcf7f62bcee3e7216adc Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 12 Mar 2015 14:38:55 -0300 Subject: [PATCH 064/125] Delete 0001_initial.py --- oidc_provider/migrations/0001_initial.py | 100 ----------------------- 1 file changed, 100 deletions(-) delete mode 100644 oidc_provider/migrations/0001_initial.py diff --git a/oidc_provider/migrations/0001_initial.py b/oidc_provider/migrations/0001_initial.py deleted file mode 100644 index 661e032..0000000 --- a/oidc_provider/migrations/0001_initial.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - ('auth', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Client', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('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)')])), - ('_redirect_uris', models.TextField(default=b'')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Code', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('code', models.CharField(unique=True, max_length=255)), - ('expires_at', models.DateTimeField()), - ('_scope', models.TextField(default=b'')), - ('client', models.ForeignKey(to='oidc_provider.Client')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Token', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('access_token', models.CharField(unique=True, max_length=255)), - ('expires_at', models.DateTimeField()), - ('_scope', models.TextField(default=b'')), - ('_id_token', models.TextField()), - ('client', models.ForeignKey(to='oidc_provider.Client')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='UserInfo', - fields=[ - ('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('given_name', models.CharField(default=b'', max_length=255)), - ('family_name', models.CharField(default=b'', max_length=255)), - ('middle_name', models.CharField(default=b'', max_length=255)), - ('nickname', models.CharField(default=b'', max_length=255)), - ('preferred_username', models.CharField(default=b'', max_length=255)), - ('profile', models.URLField(default=b'')), - ('picture', models.URLField(default=b'')), - ('website', models.URLField(default=b'')), - ('email_verified', models.BooleanField(default=False)), - ('gender', models.CharField(default=b'', max_length=100)), - ('birthdate', models.DateField(null=True)), - ('zoneinfo', models.CharField(default=b'', max_length=100)), - ('locale', models.CharField(default=b'', max_length=100)), - ('phone_number', models.CharField(default=b'', max_length=255)), - ('phone_number_verified', models.BooleanField(default=False)), - ('address_formatted', models.CharField(default=b'', max_length=255)), - ('address_street_address', models.CharField(default=b'', max_length=255)), - ('address_locality', models.CharField(default=b'', max_length=255)), - ('address_region', models.CharField(default=b'', max_length=255)), - ('address_postal_code', models.CharField(default=b'', max_length=255)), - ('address_country', models.CharField(default=b'', max_length=255)), - ('updated_at', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='token', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), - preserve_default=True, - ), - migrations.AddField( - model_name='code', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), - preserve_default=True, - ), - ] From 09249afdf54d6e3698813189f010e69bc5f34ad0 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 13 Mar 2015 16:39:47 -0300 Subject: [PATCH 065/125] Fix MANIFEST. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index c4c6761..ff58783 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE include README.rst -recursive-include oidc_provider/templates * \ No newline at end of file +recursive-include oidc_provider * \ No newline at end of file From 11a790e90a631877a1ebafe7d064ed466cd465a6 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 13 Mar 2015 17:42:32 -0300 Subject: [PATCH 066/125] Fix setup and revert MANIFEST. --- MANIFEST.in | 2 +- setup.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index ff58783..c4c6761 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE include README.rst -recursive-include oidc_provider * \ No newline at end of file +recursive-include oidc_provider/templates * \ No newline at end of file diff --git a/setup.py b/setup.py index 9e70fb7..b337062 100644 --- a/setup.py +++ b/setup.py @@ -9,8 +9,11 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', - version='0.0.0', - packages=['oidc_provider'], + version='0.0.1', + packages=[ + 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', + 'oidc_provider/lib/utils', 'oidc_provider/tests', + ], include_package_data=True, license='MIT License', description='OpenID Connect Provider implementation for Django.', From 8578528f91ae8e60cf0fabad79be80c0e3334c3d Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 13 Mar 2015 17:50:24 -0300 Subject: [PATCH 067/125] Update CHANGELOG. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c64b1..73b37b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +### [0.0.1] - 2015-03-13 + ##### Added - Provider Configuration Information endpoint. - Setting OIDC_IDTOKEN_SUB_GENERATOR. From 2d3bf16b586a1bfd161122228d75e7c96829e1ff Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 19 Mar 2015 14:04:32 -0300 Subject: [PATCH 068/125] Add OIDC_AFTER_USERLOGIN_HOOK setting. --- CHANGELOG.md | 3 +++ DOC.md | 15 +++++++++++++++ oidc_provider/settings.py | 10 ++++++++++ oidc_provider/views.py | 9 ++++++++- 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73b37b9..14b3e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +##### Added +- Setting OIDC_AFTER_USERLOGIN_HOOK. + ### [0.0.1] - 2015-03-13 ##### Added diff --git a/DOC.md b/DOC.md index 795082f..06cc0d6 100644 --- a/DOC.md +++ b/DOC.md @@ -19,6 +19,7 @@ Before getting started there are some important things that you should know: - [Settings](#settings) - [SITE_URL](#site_url) - [LOGIN_URL](#login_url) + - [OIDC_AFTER_USERLOGIN_HOOK](#oidc_after_userlogin_hook) - [OIDC_CODE_EXPIRE](#oidc_code_expire) - [OIDC_EXTRA_SCOPE_CLAIMS](#oidc_extra_scope_claims) - [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire) @@ -80,6 +81,20 @@ REQUIRED. Used to log the user in. [Read more in Django docs](https://docs.djang Default is `/accounts/login/`. +##### OIDC_AFTER_USERLOGIN_HOOK +OPTIONAL. Provide a way to plug into the process after the user has logged in, typically to perform some business logic. + +Default is: +```python +def default_hook_func(request, user, client): + return None +``` + +Return `None` if you want to continue with the flow. + +The typical situation will be checking some state of the user or maybe redirect him somewhere. +With request you have access to all OIDC parameters. Remember that if you redirect the user to another place then you need to take him back to the authorize endpoint (use `request.get_full_path()` as the value for a "next" parameter). + ##### OIDC_CODE_EXPIRE OPTIONAL. Expressed in seconds. diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 80959c5..26288b8 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -17,6 +17,16 @@ class DefaultSettings(object): """ return None + @property + def OIDC_AFTER_USERLOGIN_HOOK(self): + """ + OPTIONAL. + """ + def default_hook_func(request, user, client): + return None + + return default_hook_func + @property def OIDC_CODE_EXPIRE(self): """ diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 928b838..4850e30 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -22,6 +22,12 @@ class AuthorizeView(View): authorize.validate_params() if request.user.is_authenticated(): + # Check if there's a hook setted. + hook_resp = settings.get('OIDC_AFTER_USERLOGIN_HOOK')( + request=request, user=request.user, + client=authorize.client) + if hook_resp: + return hook_resp # Generate hidden inputs for the form. context = { @@ -30,7 +36,8 @@ class AuthorizeView(View): hidden_inputs = render_to_string( 'oidc_provider/hidden_inputs.html', context) - # Remove openid from scope list since we don't need to print it. + # Remove `openid` from scope list + # since we don't need to print it. authorize.params.scope.remove('openid') context = { From 91c9c4158c4c9bf0947036c47e18adf40a573ac0 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 19 Mar 2015 14:10:30 -0300 Subject: [PATCH 069/125] Remove unnecessary tag in authorize template. --- oidc_provider/templates/oidc_provider/authorize.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/oidc_provider/templates/oidc_provider/authorize.html b/oidc_provider/templates/oidc_provider/authorize.html index 69cede7..95591b4 100644 --- a/oidc_provider/templates/oidc_provider/authorize.html +++ b/oidc_provider/templates/oidc_provider/authorize.html @@ -18,5 +18,3 @@ - -{% endblock %} \ No newline at end of file From f85a4c1d7f42c33ed515d2c7998ca6663c266660 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 19 Mar 2015 14:19:27 -0300 Subject: [PATCH 070/125] No need of that naming in authorize tests. --- oidc_provider/tests/test_authorize_endpoint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 4946f0a..5360aaf 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -71,7 +71,7 @@ class AuthorizationCodeFlowTestCase(TestCase): query_exists = 'error=' in response['Location'] self.assertEqual(query_exists, True) - def test_codeflow_user_not_logged(self): + def test_user_not_logged(self): """ The Authorization Server attempts to Authenticate the End-User by redirecting to the login view. @@ -106,7 +106,7 @@ class AuthorizationCodeFlowTestCase(TestCase): is_next_ok = False self.assertEqual(is_next_ok, True) - def test_codeflow_user_consent_inputs(self): + def test_user_consent_inputs(self): """ Once the End-User is authenticated, the Authorization Server MUST obtain an authorization decision before releasing information to @@ -148,7 +148,7 @@ class AuthorizationCodeFlowTestCase(TestCase): self.assertEqual(is_input_ok, True, msg='Hidden input for "'+key+'" fails.') - def test_codeflow_user_consent_response(self): + def test_user_consent_response(self): """ First, if the user denied the consent we must ensure that From 45c5fe7a86754052c36b79423905e68271f7394e Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 19 Mar 2015 17:10:00 -0300 Subject: [PATCH 071/125] Add example app. --- example_app/.gitignore | 1 + example_app/README.md | 23 +++++++++ example_app/manage.py | 10 ++++ example_app/provider_app/__init__.py | 0 example_app/provider_app/settings.py | 76 ++++++++++++++++++++++++++++ example_app/provider_app/urls.py | 8 +++ example_app/provider_app/wsgi.py | 5 ++ example_app/requirements.txt | 2 + 8 files changed, 125 insertions(+) create mode 100644 example_app/.gitignore create mode 100644 example_app/README.md create mode 100755 example_app/manage.py create mode 100644 example_app/provider_app/__init__.py create mode 100644 example_app/provider_app/settings.py create mode 100644 example_app/provider_app/urls.py create mode 100644 example_app/provider_app/wsgi.py create mode 100644 example_app/requirements.txt diff --git a/example_app/.gitignore b/example_app/.gitignore new file mode 100644 index 0000000..6e9bc0c --- /dev/null +++ b/example_app/.gitignore @@ -0,0 +1 @@ +*.sqlite3 \ No newline at end of file diff --git a/example_app/README.md b/example_app/README.md new file mode 100644 index 0000000..34dc26d --- /dev/null +++ b/example_app/README.md @@ -0,0 +1,23 @@ +# Example Django App + +This is just a simple Django app with all the necessary things to work with `django-oidc-provider` package. + +## Setup & Running + +Setup project environment with [virtualenv](https://virtualenv.pypa.io) and [pip](https://pip.pypa.io). + +```bash +$ virtualenv project_env +$ source project_env/bin/activate +$ git clone https://github.com/juanifioren/django-oidc-provider.git +$ cd django-oidc-provider/example_app +$ pip install -r requirements.txt +``` + +Run your provider. + +```bash +$ python manage.py makemigrations +$ python manage.py migrate +$ python manage.py runserver +``` \ No newline at end of file diff --git a/example_app/manage.py b/example_app/manage.py new file mode 100755 index 0000000..ff8c2cf --- /dev/null +++ b/example_app/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "provider_app.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/example_app/provider_app/__init__.py b/example_app/provider_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example_app/provider_app/settings.py b/example_app/provider_app/settings.py new file mode 100644 index 0000000..ea08d5f --- /dev/null +++ b/example_app/provider_app/settings.py @@ -0,0 +1,76 @@ +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'waw%j=vza!vc1^eyosw%#_!gg96%zb7sp*+!owkutue4i(sm91' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'oidc_provider', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'provider_app.urls' + +WSGI_APPLICATION = 'provider_app.wsgi.application' + + +# Database + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Internationalization + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) + +STATIC_URL = '/static/' + + +# OIDC Provider settings. + +SITE_URL = 'http://localhost:8000' \ No newline at end of file diff --git a/example_app/provider_app/urls.py b/example_app/provider_app/urls.py new file mode 100644 index 0000000..747fd50 --- /dev/null +++ b/example_app/provider_app/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin + +urlpatterns = patterns('', + url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), + + url(r'^admin/', include(admin.site.urls)), +) diff --git a/example_app/provider_app/wsgi.py b/example_app/provider_app/wsgi.py new file mode 100644 index 0000000..cfd9ffa --- /dev/null +++ b/example_app/provider_app/wsgi.py @@ -0,0 +1,5 @@ +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "provider_app.settings") + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() diff --git a/example_app/requirements.txt b/example_app/requirements.txt new file mode 100644 index 0000000..3397367 --- /dev/null +++ b/example_app/requirements.txt @@ -0,0 +1,2 @@ +django==1.7.7 +django-oidc-provider==0.0.1 From 8c458fb028f0f4a623c316997d20b09681c149b0 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 20 Mar 2015 12:48:46 -0300 Subject: [PATCH 072/125] Update CHANGELOG. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b3e52..80ced32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. ##### Added - Setting OIDC_AFTER_USERLOGIN_HOOK. +##### Fixed +- Tests failing because an incorrect tag in one template. + ### [0.0.1] - 2015-03-13 ##### Added From b7d4c4377d03b901d5c6b8d16964ac159281cb70 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 20 Mar 2015 14:38:28 -0300 Subject: [PATCH 073/125] Reuse the scope function in models. --- oidc_provider/models.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 9bd31d4..ca8371b 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -5,6 +5,15 @@ from django.utils import timezone from django.contrib.auth.models import User +def scope_property(): + def fget(self): + return self._scope.split() + def fset(self, value): + self._scope = ' '.join(value) + + return locals() + + class Client(models.Model): RESPONSE_TYPE_CHOICES = [ @@ -42,14 +51,7 @@ class Code(models.Model): expires_at = models.DateTimeField() _scope = models.TextField(default='') - - def scope(): - def fget(self): - return self._scope.split() - def fset(self, value): - self._scope = ' '.join(value) - return locals() - scope = property(**scope()) + scope = property(**scope_property()) def has_expired(self): return timezone.now() >= self.expires_at @@ -63,14 +65,7 @@ class Token(models.Model): expires_at = models.DateTimeField() _scope = models.TextField(default='') - - def scope(): - def fget(self): - return self._scope.split() - def fset(self, value): - self._scope = ' '.join(value) - return locals() - scope = property(**scope()) + scope = property(**scope_property()) _id_token = models.TextField() From 8dbd586f42dd073bc2f85f75b518aa36f9c15c9e Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 25 Mar 2015 11:35:59 -0300 Subject: [PATCH 074/125] Update Docs. --- DOC.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DOC.md b/DOC.md index 06cc0d6..693a77f 100644 --- a/DOC.md +++ b/DOC.md @@ -1,5 +1,3 @@ -![OpenID Connect](http://wiki.openid.net/f/openid-logo-wordmark.png) - # Welcome to the Docs! Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. @@ -36,6 +34,8 @@ Before getting started there are some important things that you should know: ## Installation +If you want to get started fast see our [Example App](https://github.com/juanifioren/django-oidc-provider/tree/master/example_app) folder. + Install the package using pip. ```bash From 724dcfdc1d1fa2b8d6498b810ad89cef7ff043ec Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 25 Mar 2015 14:49:08 -0300 Subject: [PATCH 075/125] Update README. --- README.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.rst b/README.rst index 915088b..cc1baa4 100644 --- a/README.rst +++ b/README.rst @@ -12,16 +12,6 @@ See changelog here. https://github.com/juanifioren/django-oidc-provider/blob/master/CHANGELOG.md -************* -Running tests -************* - -You need a Django project properly configured with the package. Then just run tests as normal. - -.. code:: - - $ python manage.py test oidc_provider - ************ Contributing ************ From 363b654a58d9722e12ae847d0f7dd05133485fdd Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 25 Mar 2015 14:54:11 -0300 Subject: [PATCH 076/125] Update Docs. --- DOC.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DOC.md b/DOC.md index 693a77f..73fce25 100644 --- a/DOC.md +++ b/DOC.md @@ -2,8 +2,11 @@ Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. + **This project is still in DEVELOPMENT and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES, unless you know what you do.** +**************************************** + Before getting started there are some important things that you should know: * Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. * Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. @@ -26,6 +29,7 @@ Before getting started there are some important things that you should know: - [Users And Clients](#users-and-clients) - [Templates](#templates) - [Server Endpoints](#server-endpoints) +- [Running Tests](#running-tests) ## Requirements @@ -261,3 +265,11 @@ POST /openid/userinfo/ HTTP/1.1 Host: localhost:8000 Authorization: Bearer [ACCESS_TOKEN] ``` + +## Running Tests + +You need a Django project properly configured with the package. Then just run tests as normal. + +```bash +$ python manage.py test oidc_provider +``` From 44d0e9c31c8875fa71af751c0d9e6e852fd89e14 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 25 Mar 2015 18:11:34 -0300 Subject: [PATCH 077/125] Update CHANGELOG. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ced32..4e80993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +### [0.0.2] - 2015-03-26 + ##### Added - Setting OIDC_AFTER_USERLOGIN_HOOK. From 91896ec89bcd6b69f5f67372ee970d82b016d637 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 25 Mar 2015 18:13:23 -0300 Subject: [PATCH 078/125] Update version in setup. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b337062..2867545 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', - version='0.0.1', + version='0.0.2', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider/lib/utils', 'oidc_provider/tests', From dd366479d4285e94fc61df51291042baa8a2e3c6 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 27 Mar 2015 12:22:56 -0300 Subject: [PATCH 079/125] Add folder to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 61d98f0..75d9974 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ +build/ dist/ *.py[cod] *.egg-info From 0df97dfb7297d20adc09797ec3bb2c1563a6c47b Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 27 Mar 2015 14:42:41 -0300 Subject: [PATCH 080/125] Add choices to gender in UserInfo. --- oidc_provider/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index ca8371b..3e7c18b 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -80,12 +80,17 @@ class Token(models.Model): class UserInfo(models.Model): + GENDER_CHOICES = [ + ('F', 'Female'), + ('M', 'Male'), + ] + user = models.OneToOneField(User, primary_key=True) given_name = models.CharField(max_length=255, blank=True, null=True) family_name = models.CharField(max_length=255, blank=True, null=True) middle_name = models.CharField(max_length=255, blank=True, null=True) nickname = models.CharField(max_length=255, blank=True, null=True) - gender = models.CharField(max_length=100, default='', blank=True, null=True) + gender = models.CharField(max_length=100, choices=GENDER_CHOICES, null=True) birthdate = models.DateField(null=True) zoneinfo = models.CharField(max_length=100, default='', blank=True, null=True) From d3824bbe975475235a048d426b76c3dc51b9cd15 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 27 Mar 2015 15:07:49 -0300 Subject: [PATCH 081/125] Edit CHANGELOG. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e80993..eb1869f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +##### Added +- Normalize gender field in UserInfo. + ### [0.0.2] - 2015-03-26 ##### Added From 07e80d5d5ccc5ed6f3fa564ebf06f65ce33e3761 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 30 Mar 2015 15:37:48 -0300 Subject: [PATCH 082/125] Fix in tests when setting a hook. --- .../tests/test_authorize_endpoint.py | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 5360aaf..0deffa0 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -15,8 +15,7 @@ from oidc_provider.views import * class AuthorizationCodeFlowTestCase(TestCase): """ - An Authentication Request is an OAuth 2.0 Authorization Request that - requests that the End-User be authenticated by the Authorization Server. + Test cases for Authorize Endpoint using Authorization Code Flow. """ def setUp(self): @@ -114,14 +113,11 @@ class AuthorizationCodeFlowTestCase(TestCase): See: http://openid.net/specs/openid-connect-core-1_0.html#Consent """ - response_type = 'code' - state = 'openid email' - query_str = urllib.urlencode({ 'client_id': self.client.client_id, - 'response_type': response_type, + 'response_type': 'code', 'redirect_uri': self.client.default_redirect_uri, - 'scope': state, + 'scope': 'openid email', 'state': self.state, }).replace('+', '%20') @@ -131,7 +127,11 @@ class AuthorizationCodeFlowTestCase(TestCase): # Simulate that the user is logged. request.user = self.user - response = AuthorizeView.as_view()(request) + # Remove the hook, because we want to test default behaviour. + OIDC_AFTER_USERLOGIN_HOOK = settings.default_settings.OIDC_AFTER_USERLOGIN_HOOK + with self.settings( + OIDC_AFTER_USERLOGIN_HOOK=OIDC_AFTER_USERLOGIN_HOOK): + response = AuthorizeView.as_view()(request) # Check if hidden inputs exists in the form, # also if their values are valid. @@ -140,7 +140,7 @@ class AuthorizationCodeFlowTestCase(TestCase): to_check = { 'client_id': self.client.client_id, 'redirect_uri': self.client.default_redirect_uri, - 'response_type': response_type, + 'response_type': 'code', } for key, value in to_check.iteritems(): @@ -213,6 +213,29 @@ class AuthorizationCodeFlowTestCase(TestCase): class AuthorizationImplicitFlowTestCase(TestCase): """ - TODO. + Test cases for Authorize Endpoint using Implicit Flow. """ - pass \ No newline at end of file + + def setUp(self): + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='id_token token') + self.state = uuid.uuid4().hex + + # TODO + def test_something(self): + query_str = urllib.urlencode({ + 'client_id': self.client.client_id, + 'response_type': 'id_token token', + 'redirect_uri': self.client.default_redirect_uri, + 'scope': 'openid email', + 'state': self.state, + }).replace('+', '%20') + + url = reverse('oidc_provider:authorize') + '#' + query_str + + request = self.factory.get(url) + # Simulate that the user is logged. + request.user = self.user + + response = AuthorizeView.as_view()(request) \ No newline at end of file From a1df576d1c7391e8eeb4b3379071ce2195a2204b Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 31 Mar 2015 15:31:17 -0300 Subject: [PATCH 083/125] Important fix in recursive function. --- oidc_provider/lib/claims.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc_provider/lib/claims.py b/oidc_provider/lib/claims.py index e9d855a..5977e14 100644 --- a/oidc_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -56,7 +56,7 @@ class AbstractScopeClaims(object): if not value: del aux_dic[key] elif type(value) is dict: - aux_dic[key] = clean_dic(value) + aux_dic[key] = self._clean_dic(value) return aux_dic From d92948ef28fdc0497480655a06db30bc90b115e2 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Tue, 31 Mar 2015 15:45:45 -0300 Subject: [PATCH 084/125] Update CHANGELOG. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1869f..17d62c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. ##### Added - Normalize gender field in UserInfo. +##### Fixed +- Important bug in claims response. + ### [0.0.2] - 2015-03-26 ##### Added From 63cb8351b8d676d41f444248dbed773387c2ae4c Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 15 Apr 2015 14:54:42 -0300 Subject: [PATCH 085/125] Make address_formatted a property inside UserInfo model. --- oidc_provider/models.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 3e7c18b..120f010 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -103,7 +103,6 @@ class UserInfo(models.Model): locale = models.CharField(max_length=100, blank=True, null=True) phone_number = models.CharField(max_length=255, blank=True, null=True) phone_number_verified = models.NullBooleanField(default=False) - address_formatted = models.CharField(max_length=255, blank=True, null=True) address_street_address = models.CharField(max_length=255, blank=True, null=True) address_locality = models.CharField(max_length=255, blank=True, null=True) @@ -122,3 +121,15 @@ class UserInfo(models.Model): name = name + ' ' + self.family_name return name + + @property + def address_formatted(self): + formatted = ', '.join([ + self.address_street_address, + self.address_locality, + self.address_country]) + + if formatted.startswith(', '): + formatted = formatted[2:] + if formatted.endswith(', '): + formatted = formatted[:-2] \ No newline at end of file From be573adea6d4a716ced4051585a6dfb98147b083 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 15 Apr 2015 16:29:26 -0300 Subject: [PATCH 086/125] Update CHANGELOG. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d62c7..61abe09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. ##### Added - Normalize gender field in UserInfo. +##### Changed +- Make address_formatted a property inside UserInfo. + ##### Fixed - Important bug in claims response. From 04b417f744bdbb3f55aebe56bee9fe3488a243cf Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 15 Apr 2015 16:47:02 -0300 Subject: [PATCH 087/125] Bump version 0.0.3. --- CHANGELOG.md | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61abe09..8eeb30f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +### [0.0.3] - 2015-04-15 + ##### Added - Normalize gender field in UserInfo. diff --git a/setup.py b/setup.py index 2867545..0852393 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', - version='0.0.2', + version='0.0.3', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider/lib/utils', 'oidc_provider/tests', From b107724b9ae94760c0f341f0d72c70d273f92a6b Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 15 Apr 2015 21:53:52 -0300 Subject: [PATCH 088/125] Fix Docs. --- DOC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOC.md b/DOC.md index 73fce25..a9a3c4b 100644 --- a/DOC.md +++ b/DOC.md @@ -125,7 +125,7 @@ class MyAppScopeClaims(AbstractScopeClaims): # print self.scopes try: self.some_model = SomeModel.objects.get(user=self.user) - except UserInfo.DoesNotExist: + except SomeModel.DoesNotExist: # Create an empty model object. self.some_model = SomeModel() From 4c4677af8ed013504956928d9cb17455c1c9b80c Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 16 Apr 2015 16:02:24 -0300 Subject: [PATCH 089/125] Update Docs. --- DOC.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/DOC.md b/DOC.md index a9a3c4b..469b2ce 100644 --- a/DOC.md +++ b/DOC.md @@ -216,8 +216,6 @@ You can copy the sample html here and edit them with your own styles. - -{% endblock %} ``` **error.html** From c521e81722d385758d8ca08009c3c96b76584a15 Mon Sep 17 00:00:00 2001 From: Francois Gaudin Date: Fri, 17 Apr 2015 13:02:10 -0700 Subject: [PATCH 090/125] Adding models into the admin --- oidc_provider/admin.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 oidc_provider/admin.py diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py new file mode 100644 index 0000000..f98d4d4 --- /dev/null +++ b/oidc_provider/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import Client, Code, Token, UserInfo + +admin.site.register(Client) +admin.site.register(Code) +admin.site.register(Token) +admin.site.register(UserInfo) \ No newline at end of file From 4c16097f4054cb36b12a2c7d1351e2f7c249cb38 Mon Sep 17 00:00:00 2001 From: Francois Gaudin Date: Fri, 17 Apr 2015 15:39:55 -0700 Subject: [PATCH 091/125] Fixed id_token dict in implicit flow --- oidc_provider/lib/endpoints/authorize.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index d6682f5..ff2e206 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -4,6 +4,7 @@ import uuid from django.utils import timezone from oidc_provider.lib.errors import * +from oidc_provider.lib.utils.common import get_issuer from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.token import * from oidc_provider.models import * @@ -107,10 +108,15 @@ class AuthorizeEndpoint(object): else: # Implicit Flow - id_token_dic = create_id_token_dic( - self.request.user, - settings.get('SITE_URL'), - self.client.client_id) + # TODO refactor since it's the same as the token endpoint + sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( + user=self.request.user) + + id_token_dic = create_id_token( + iss=get_issuer(), + sub=sub, + aud=self.client.client_id, + auth_time=self.request.user.last_login) token = create_token( user=self.request.user, From ec7840cc14a8a1ee70ab0b80dda822ec4e3d2282 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Sat, 18 Apr 2015 17:36:11 -0300 Subject: [PATCH 092/125] Update CHANGELOG. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eeb30f..624deaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +##### Fixed +- Important bug with id_token when using implicit flow. + ### [0.0.3] - 2015-04-15 ##### Added From 989b110bf0a0681db45036cd1a130634b278d971 Mon Sep 17 00:00:00 2001 From: Nicco Date: Mon, 20 Apr 2015 13:49:45 -0300 Subject: [PATCH 093/125] Update setup.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Utilizar una versión reciente de pywt, funciona igual , adicionalmente se evita incompatibilidades por dependencia de una mayor version de parte de otros packages como python-social. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0852393..e0ef786 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,6 @@ setup( 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], install_requires=[ - 'pyjwt==0.3.1', + 'pyjwt==1.1.0', ], ) From 9b321fef9aa127c8a3f80e89444b6a5d7152651b Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 21 Apr 2015 14:28:59 -0300 Subject: [PATCH 094/125] Fix conditional for code expiration. --- oidc_provider/lib/endpoints/token.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 47437ae..e069577 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -13,13 +13,11 @@ from oidc_provider import settings class TokenEndpoint(object): def __init__(self, request): - self.request = request self.params = Params() self._extract_params() def _extract_params(self): - query_dict = self.request.POST self.params.client_id = query_dict.get('client_id', '') @@ -31,7 +29,6 @@ class TokenEndpoint(object): self.params.state = query_dict.get('state', '') def validate_params(self): - if not (self.params.grant_type == 'authorization_code'): raise TokenError('unsupported_grant_type') @@ -46,8 +43,8 @@ class TokenEndpoint(object): self.code = Code.objects.get(code=self.params.code) - if not (self.code.client == self.client) and \ - not self.code.has_expired(): + if not (self.code.client == self.client) \ + or self.code.has_expired(): raise TokenError('invalid_grant') except Client.DoesNotExist: @@ -57,7 +54,6 @@ class TokenEndpoint(object): raise TokenError('invalid_grant') def create_response_dic(self): - sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( user=self.code.user) From 0f03bdfb672c73502c51da979d80dde9cbc0715f Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 21 Apr 2015 15:19:43 -0300 Subject: [PATCH 095/125] Add abstract class for Code and Token models. --- oidc_provider/models.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 120f010..3b204e3 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -5,15 +5,6 @@ from django.utils import timezone from django.contrib.auth.models import User -def scope_property(): - def fget(self): - return self._scope.split() - def fset(self, value): - self._scope = ' '.join(value) - - return locals() - - class Client(models.Model): RESPONSE_TYPE_CHOICES = [ @@ -43,32 +34,36 @@ class Client(models.Model): return self.redirect_uris[0] if self.redirect_uris else '' -class Code(models.Model): +class BaseCodeTokenModel(models.Model): user = models.ForeignKey(User) client = models.ForeignKey(Client) - code = models.CharField(max_length=255, unique=True) expires_at = models.DateTimeField() - _scope = models.TextField(default='') - scope = property(**scope_property()) + def scope(): + def fget(self): + return self._scope.split() + def fset(self, value): + self._scope = ' '.join(value) + return locals() + scope = property(**scope()) def has_expired(self): return timezone.now() >= self.expires_at + class Meta: + abstract = True -class Token(models.Model): - user = models.ForeignKey(User) - client = models.ForeignKey(Client) +class Code(BaseCodeTokenModel): + + code = models.CharField(max_length=255, unique=True) + + +class Token(BaseCodeTokenModel): + access_token = models.CharField(max_length=255, unique=True) - expires_at = models.DateTimeField() - - _scope = models.TextField(default='') - scope = property(**scope_property()) - _id_token = models.TextField() - def id_token(): def fget(self): return json.loads(self._id_token) From b317eae7e1a49f1de4ea1b186a42ea06568a95d4 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Tue, 21 Apr 2015 15:26:32 -0300 Subject: [PATCH 096/125] Update CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 624deaf..0ab8a83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ##### Fixed - Important bug with id_token when using implicit flow. +- Validate Code expiration. ### [0.0.3] - 2015-04-15 From d30df27151629b2e99caa36ec15d69042fa2f794 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Tue, 21 Apr 2015 15:27:02 -0300 Subject: [PATCH 097/125] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab8a83..e988b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ##### Fixed - Important bug with id_token when using implicit flow. -- Validate Code expiration. +- Validate Code expiration in Auth Code Flow. ### [0.0.3] - 2015-04-15 From 180e0183c5e2fc4e3cdbc5df22040f41d6495923 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 21 Apr 2015 17:14:26 -0300 Subject: [PATCH 098/125] Validate expiration of access_token in UserInfo. --- oidc_provider/lib/endpoints/userinfo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oidc_provider/lib/endpoints/userinfo.py b/oidc_provider/lib/endpoints/userinfo.py index 3a7a48c..61e10e5 100644 --- a/oidc_provider/lib/endpoints/userinfo.py +++ b/oidc_provider/lib/endpoints/userinfo.py @@ -45,6 +45,9 @@ class UserInfoEndpoint(object): try: self.token = Token.objects.get(access_token=self.params.access_token) + if self.token.has_expired(): + raise UserInfoError('invalid_token') + except Token.DoesNotExist: raise UserInfoError('invalid_token') From b3cf751195c58fefbcf9cf98a4b5df75cfc10771 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Tue, 21 Apr 2015 17:29:41 -0300 Subject: [PATCH 099/125] Update CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e988b45..837bdc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ##### Fixed - Important bug with id_token when using implicit flow. - Validate Code expiration in Auth Code Flow. +- Validate Access Token expiration in UserInfo endpoint. ### [0.0.3] - 2015-04-15 From 99ec0675eedb8e0d80dd00d5d6b8e931b343d883 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 21 Apr 2015 17:43:00 -0300 Subject: [PATCH 100/125] Add missing previously removed migration. --- oidc_provider/migrations/0001_initial.py | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 oidc_provider/migrations/0001_initial.py diff --git a/oidc_provider/migrations/0001_initial.py b/oidc_provider/migrations/0001_initial.py new file mode 100644 index 0000000..ca32b7e --- /dev/null +++ b/oidc_provider/migrations/0001_initial.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Client', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('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)')])), + ('_redirect_uris', models.TextField(default=b'')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Code', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('expires_at', models.DateTimeField()), + ('_scope', models.TextField(default=b'')), + ('code', models.CharField(unique=True, max_length=255)), + ('client', models.ForeignKey(to='oidc_provider.Client')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Token', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('expires_at', models.DateTimeField()), + ('_scope', models.TextField(default=b'')), + ('access_token', models.CharField(unique=True, max_length=255)), + ('_id_token', models.TextField()), + ('client', models.ForeignKey(to='oidc_provider.Client')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='UserInfo', + fields=[ + ('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('given_name', models.CharField(max_length=255, null=True, blank=True)), + ('family_name', models.CharField(max_length=255, null=True, blank=True)), + ('middle_name', models.CharField(max_length=255, null=True, blank=True)), + ('nickname', models.CharField(max_length=255, null=True, blank=True)), + ('gender', models.CharField(max_length=100, null=True, choices=[(b'F', b'Female'), (b'M', b'Male')])), + ('birthdate', models.DateField(null=True)), + ('zoneinfo', models.CharField(default=b'', max_length=100, null=True, blank=True)), + ('preferred_username', models.CharField(max_length=255, null=True, blank=True)), + ('profile', models.URLField(default=b'', null=True, blank=True)), + ('picture', models.URLField(default=b'', null=True, blank=True)), + ('website', models.URLField(default=b'', null=True, blank=True)), + ('email_verified', models.NullBooleanField(default=False)), + ('locale', models.CharField(max_length=100, null=True, blank=True)), + ('phone_number', models.CharField(max_length=255, null=True, blank=True)), + ('phone_number_verified', models.NullBooleanField(default=False)), + ('address_street_address', models.CharField(max_length=255, null=True, blank=True)), + ('address_locality', models.CharField(max_length=255, null=True, blank=True)), + ('address_region', models.CharField(max_length=255, null=True, blank=True)), + ('address_postal_code', models.CharField(max_length=255, null=True, blank=True)), + ('address_country', models.CharField(max_length=255, null=True, blank=True)), + ('updated_at', models.DateTimeField(auto_now=True, null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='token', + name='user', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + migrations.AddField( + model_name='code', + name='user', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + ] From 303df2b3d21e01c27fef24e23cd037081a3b8243 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 22 Apr 2015 11:48:47 -0300 Subject: [PATCH 101/125] Update CHANGELOG. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 837bdc8..f72f1eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +#### Added +- Initial migrations. + ##### Fixed - Important bug with id_token when using implicit flow. - Validate Code expiration in Auth Code Flow. From e773083e7d4b5dd005148259d2eb7f07c1b5c80f Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 22 Apr 2015 11:53:19 -0300 Subject: [PATCH 102/125] Bump version 0.0.4. --- CHANGELOG.md | 2 ++ oidc_provider/admin.py | 4 +++- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f72f1eb..7c55c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +### [0.0.4] - 2015-04-22 + #### Added - Initial migrations. diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py index f98d4d4..ba03af5 100644 --- a/oidc_provider/admin.py +++ b/oidc_provider/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin -from .models import Client, Code, Token, UserInfo + +from oidc_provider.models import Client, Code, Token, UserInfo + admin.site.register(Client) admin.site.register(Code) diff --git a/setup.py b/setup.py index e0ef786..d0a1ad5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', - version='0.0.3', + version='0.0.4', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider/lib/utils', 'oidc_provider/tests', From f0579373b2c3261b00228d4c30832f51d713a7c2 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 22 Apr 2015 12:17:24 -0300 Subject: [PATCH 103/125] Add migrations folder to build. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d0a1ad5..58ff139 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( version='0.0.4', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', - 'oidc_provider/lib/utils', 'oidc_provider/tests', + 'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/migrations', ], include_package_data=True, license='MIT License', From af70f4cc99083f618508925a800a92f837c05778 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 22 Apr 2015 16:18:58 -0300 Subject: [PATCH 104/125] Update Docs --- DOC.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DOC.md b/DOC.md index 469b2ce..e613e90 100644 --- a/DOC.md +++ b/DOC.md @@ -271,3 +271,7 @@ You need a Django project properly configured with the package. Then just run te ```bash $ python manage.py test oidc_provider ``` + +This provider was tested (and fully works) with these OIDC Clients: +- [Drupal OpenID Connect](https://www.drupal.org/project/openid_connect) +- [Passport OpenID Connect](https://github.com/jaredhanson/passport-openidconnect) (for NodeJS) From d5749739d652ca57e7d3004dca137adb508f8685 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 28 Apr 2015 14:55:35 -0300 Subject: [PATCH 105/125] Update example project. --- example_app/requirements.txt | 2 - {example_app => example_project}/.gitignore | 0 {example_app => example_project}/README.md | 0 {example_app => example_project}/manage.py | 0 .../provider_app/__init__.py | 0 .../provider_app/settings.py | 1 + .../templates/accounts/login.html | 24 +++++++++ .../provider_app/templates/base.html | 50 +++++++++++++++++++ .../templates/oidc_provider/authorize.html | 30 +++++++++++ .../templates/oidc_provider/error.html | 14 ++++++ .../provider_app/urls.py | 4 ++ .../provider_app/wsgi.py | 0 example_project/requirements.txt | 2 + 13 files changed, 125 insertions(+), 2 deletions(-) delete mode 100644 example_app/requirements.txt rename {example_app => example_project}/.gitignore (100%) rename {example_app => example_project}/README.md (100%) rename {example_app => example_project}/manage.py (100%) rename {example_app => example_project}/provider_app/__init__.py (100%) rename {example_app => example_project}/provider_app/settings.py (96%) create mode 100644 example_project/provider_app/templates/accounts/login.html create mode 100644 example_project/provider_app/templates/base.html create mode 100644 example_project/provider_app/templates/oidc_provider/authorize.html create mode 100644 example_project/provider_app/templates/oidc_provider/error.html rename {example_app => example_project}/provider_app/urls.py (60%) rename {example_app => example_project}/provider_app/wsgi.py (100%) create mode 100644 example_project/requirements.txt diff --git a/example_app/requirements.txt b/example_app/requirements.txt deleted file mode 100644 index 3397367..0000000 --- a/example_app/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -django==1.7.7 -django-oidc-provider==0.0.1 diff --git a/example_app/.gitignore b/example_project/.gitignore similarity index 100% rename from example_app/.gitignore rename to example_project/.gitignore diff --git a/example_app/README.md b/example_project/README.md similarity index 100% rename from example_app/README.md rename to example_project/README.md diff --git a/example_app/manage.py b/example_project/manage.py similarity index 100% rename from example_app/manage.py rename to example_project/manage.py diff --git a/example_app/provider_app/__init__.py b/example_project/provider_app/__init__.py similarity index 100% rename from example_app/provider_app/__init__.py rename to example_project/provider_app/__init__.py diff --git a/example_app/provider_app/settings.py b/example_project/provider_app/settings.py similarity index 96% rename from example_app/provider_app/settings.py rename to example_project/provider_app/settings.py index ea08d5f..33dc4f0 100644 --- a/example_app/provider_app/settings.py +++ b/example_project/provider_app/settings.py @@ -26,6 +26,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', + 'provider_app', 'oidc_provider', ) diff --git a/example_project/provider_app/templates/accounts/login.html b/example_project/provider_app/templates/accounts/login.html new file mode 100644 index 0000000..6c24774 --- /dev/null +++ b/example_project/provider_app/templates/accounts/login.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+
+ {% csrf_token %} + + {% if form.errors %} + + {% endif %} +
+ +
+
+ +
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/example_project/provider_app/templates/base.html b/example_project/provider_app/templates/base.html new file mode 100644 index 0000000..6179ad7 --- /dev/null +++ b/example_project/provider_app/templates/base.html @@ -0,0 +1,50 @@ + + + + + + + Bootstrap 101 Template + + + + + + + + + + +
+
+ +

django-oidc-provider

+
+ + {% block content %}{% endblock %} + + + +
+ + + + + + + \ No newline at end of file diff --git a/example_project/provider_app/templates/oidc_provider/authorize.html b/example_project/provider_app/templates/oidc_provider/authorize.html new file mode 100644 index 0000000..4c7f807 --- /dev/null +++ b/example_project/provider_app/templates/oidc_provider/authorize.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+

Request for Permission

+
+
+

Client {{ client.name }} would like to access this information of you ...

+ +
+ + {% csrf_token %} + + {{ hidden_inputs }} + +
    + {% for scope in params.scope %} +
  • {{ scope | capfirst }}
  • + {% endfor %} +
+ + + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/example_project/provider_app/templates/oidc_provider/error.html b/example_project/provider_app/templates/oidc_provider/error.html new file mode 100644 index 0000000..b6e75dd --- /dev/null +++ b/example_project/provider_app/templates/oidc_provider/error.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+

{{ error }}

+
+
+ {{ description }} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/example_app/provider_app/urls.py b/example_project/provider_app/urls.py similarity index 60% rename from example_app/provider_app/urls.py rename to example_project/provider_app/urls.py index 747fd50..f8f435f 100644 --- a/example_app/provider_app/urls.py +++ b/example_project/provider_app/urls.py @@ -1,7 +1,11 @@ +from django.contrib.auth import views as auth_views from django.conf.urls import patterns, include, url from django.contrib import admin + urlpatterns = patterns('', + url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'), + url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), url(r'^admin/', include(admin.site.urls)), diff --git a/example_app/provider_app/wsgi.py b/example_project/provider_app/wsgi.py similarity index 100% rename from example_app/provider_app/wsgi.py rename to example_project/provider_app/wsgi.py diff --git a/example_project/requirements.txt b/example_project/requirements.txt new file mode 100644 index 0000000..32999ea --- /dev/null +++ b/example_project/requirements.txt @@ -0,0 +1,2 @@ +django==1.8 +django-oidc-provider==0.0.4.1 From b849e0fbff4ae1b8be99a6a64f36e5388eff3d2f Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 29 Apr 2015 14:02:47 -0300 Subject: [PATCH 106/125] Update Docs. --- DOC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOC.md b/DOC.md index e613e90..6a3cb31 100644 --- a/DOC.md +++ b/DOC.md @@ -38,7 +38,7 @@ Before getting started there are some important things that you should know: ## Installation -If you want to get started fast see our [Example App](https://github.com/juanifioren/django-oidc-provider/tree/master/example_app) folder. +If you want to get started fast see our [Example Project](https://github.com/juanifioren/django-oidc-provider/tree/master/example_project) folder. Install the package using pip. From 27371b271dbc5b993b1f6ea17d629e1ebec120d4 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 29 Apr 2015 14:04:57 -0300 Subject: [PATCH 107/125] Update example project README. --- example_project/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example_project/README.md b/example_project/README.md index 34dc26d..de5c475 100644 --- a/example_project/README.md +++ b/example_project/README.md @@ -1,6 +1,6 @@ -# Example Django App +# Example Project -This is just a simple Django app with all the necessary things to work with `django-oidc-provider` package. +Run your own OIDC provider in a second. This is a Django app with all the necessary things to work with `django-oidc-provider` package. ## Setup & Running @@ -9,6 +9,7 @@ Setup project environment with [virtualenv](https://virtualenv.pypa.io) and [pip ```bash $ virtualenv project_env $ source project_env/bin/activate + $ git clone https://github.com/juanifioren/django-oidc-provider.git $ cd django-oidc-provider/example_app $ pip install -r requirements.txt @@ -17,7 +18,6 @@ $ pip install -r requirements.txt Run your provider. ```bash -$ python manage.py makemigrations $ python manage.py migrate $ python manage.py runserver -``` \ No newline at end of file +``` From 4b3039ceae26c3eb338e44453629a38a21f99713 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Wed, 29 Apr 2015 18:55:48 -0300 Subject: [PATCH 108/125] Refactoring for create_id_token function. --- oidc_provider/lib/endpoints/authorize.py | 13 ++----------- oidc_provider/lib/endpoints/token.py | 10 ++-------- oidc_provider/lib/utils/token.py | 19 ++++++++++++------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index ff2e206..667de5a 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -4,7 +4,6 @@ import uuid from django.utils import timezone from oidc_provider.lib.errors import * -from oidc_provider.lib.utils.common import get_issuer from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.token import * from oidc_provider.models import * @@ -95,7 +94,6 @@ class AuthorizeEndpoint(object): self.validate_params() if self.grant_type == 'authorization_code': - code = create_code( user=self.request.user, client=self.client, @@ -107,16 +105,9 @@ class AuthorizeEndpoint(object): uri = self.params.redirect_uri + '?code={0}'.format(code.code) else: # Implicit Flow - - # TODO refactor since it's the same as the token endpoint - sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( - user=self.request.user) - id_token_dic = create_id_token( - iss=get_issuer(), - sub=sub, - aud=self.client.client_id, - auth_time=self.request.user.last_login) + user=self.request.user, + aud=self.client.client_id) token = create_token( user=self.request.user, diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index e069577..9c5f9dd 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -3,7 +3,6 @@ import urllib from django.http import JsonResponse from oidc_provider.lib.errors import * -from oidc_provider.lib.utils.common import get_issuer from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.token import * from oidc_provider.models import * @@ -54,14 +53,9 @@ class TokenEndpoint(object): raise TokenError('invalid_grant') def create_response_dic(self): - sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( - user=self.code.user) - id_token_dic = create_id_token( - iss=get_issuer(), - sub=sub, - aud=self.client.client_id, - auth_time=self.code.user.last_login) + user=self.code.user, + aud=self.client.client_id) token = create_token( user=self.code.user, diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 74d04cb..9ef7dd2 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -5,34 +5,39 @@ import uuid from django.utils import timezone import jwt +from oidc_provider.lib.utils.common import get_issuer from oidc_provider.models import * from oidc_provider import settings -def create_id_token(iss, sub, aud, auth_time): +def create_id_token(user, aud): """ - Receives a user object, iss (issuer) and aud (audience). - Then creates the id_token dic. + Receives a user object and aud (audience). + Then creates the id_token dictionary. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken Return a dic. """ + sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( + user=user) + expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') now = timezone.now() - # Convert datetimes into timestamps. iat_time = time.mktime(now.timetuple()) exp_time = time.mktime((now + timedelta(seconds=expires_in)).timetuple()) - user_auth_time = time.mktime(auth_time.timetuple()) + + user_auth_time = user.last_login or user.date_joined + auth_time = time.mktime(user_auth_time.timetuple()) dic = { - 'iss': iss, + 'iss': get_issuer(), 'sub': sub, 'aud': aud, 'exp': exp_time, 'iat': iat_time, - 'auth_time': user_auth_time, + 'auth_time': auth_time, } return dic From 2529fef5baf2fa3e2fbb2785b43d9200d452ac0b Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 30 Apr 2015 12:42:00 -0300 Subject: [PATCH 109/125] Fix test_authorize_endpoint messages. --- oidc_provider/tests/test_authorize_endpoint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 0deffa0..6b8619b 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -181,9 +181,9 @@ class AuthorizationCodeFlowTestCase(TestCase): # Because user doesn't allow app, SHOULD exists an error parameter # in the query. self.assertEqual('error=' in response['Location'], True, - msg='error param is missing.') + msg='error param is missing in query.') self.assertEqual('access_denied' in response['Location'], True, - msg='access_denied param is missing.') + msg='"access_denied" code is missing in query.') # Simulate user authorization. post_data['allow'] = 'Accept' # Should be the value of the button. @@ -238,4 +238,4 @@ class AuthorizationImplicitFlowTestCase(TestCase): # Simulate that the user is logged. request.user = self.user - response = AuthorizeView.as_view()(request) \ No newline at end of file + response = AuthorizeView.as_view()(request) From 09b3d8d6d670ba64107c112e68853cf46b76ead8 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 30 Apr 2015 12:45:04 -0300 Subject: [PATCH 110/125] Update example project README. --- example_project/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_project/README.md b/example_project/README.md index de5c475..d80bfe8 100644 --- a/example_project/README.md +++ b/example_project/README.md @@ -11,7 +11,7 @@ $ virtualenv project_env $ source project_env/bin/activate $ git clone https://github.com/juanifioren/django-oidc-provider.git -$ cd django-oidc-provider/example_app +$ cd django-oidc-provider/example_project $ pip install -r requirements.txt ``` From 79c36f3c1babd8b7ff06062a456f1184e6567939 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 30 Apr 2015 17:03:39 -0300 Subject: [PATCH 111/125] Update example_project folder. --- example_project/provider_app/settings.py | 1 + .../provider_app/templates/accounts/logout.html | 12 ++++++++++++ example_project/provider_app/templates/base.html | 8 ++++---- example_project/provider_app/templates/home.html | 11 +++++++++++ .../templates/oidc_provider/authorize.html | 2 +- example_project/provider_app/urls.py | 5 ++++- 6 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 example_project/provider_app/templates/accounts/logout.html create mode 100644 example_project/provider_app/templates/home.html diff --git a/example_project/provider_app/settings.py b/example_project/provider_app/settings.py index 33dc4f0..3c07579 100644 --- a/example_project/provider_app/settings.py +++ b/example_project/provider_app/settings.py @@ -71,6 +71,7 @@ USE_TZ = True STATIC_URL = '/static/' +LOGIN_REDIRECT_URL = '/' # OIDC Provider settings. diff --git a/example_project/provider_app/templates/accounts/logout.html b/example_project/provider_app/templates/accounts/logout.html new file mode 100644 index 0000000..25aa0f8 --- /dev/null +++ b/example_project/provider_app/templates/accounts/logout.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+

Bye!

+

Thanks for spending some quality time with the web site today.

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/example_project/provider_app/templates/base.html b/example_project/provider_app/templates/base.html index 6179ad7..6d38b8b 100644 --- a/example_project/provider_app/templates/base.html +++ b/example_project/provider_app/templates/base.html @@ -4,11 +4,11 @@ - Bootstrap 101 Template + OpenID Provider - +