diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c904932 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +omit = + tests/* + example_project/* + .tox/* + setup.py + *.egg/* + */__main__.py \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9e6b1fb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "2.7" +env: + - DJANGO=1.7.8 + - DJANGO=1.8.2 +install: + - pip install -q Django==$DJANGO --use-mirrors + - pip install pyjwt==1.1.0 --use-mirrors +script: + - PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test oidc_provider --settings=oidc_provider.tests.test_settings \ No newline at end of file diff --git a/README.rst b/README.rst index cc1baa4..7e0467c 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,10 @@ Django OIDC Provider #################### +.. image:: https://api.travis-ci.org/django-py/django-openid-provider.png?branch=master + :alt: Build Status + :target: http://travis-ci.org/django-py/django-openid-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. diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 667de5a..2ea3665 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -1,13 +1,11 @@ -from datetime import timedelta -import uuid - -from django.utils import timezone +import logging 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 + +logger = logging.getLogger(__name__) class AuthorizeEndpoint(object): @@ -134,6 +132,10 @@ class AuthorizeEndpoint(object): if self.params.response_type == 'id_token token': uri += '&access_token={0}'.format(token.access_token) except: + logger.error('Authorization server error, grant_type: %s' %self.grant_type, extra={ + 'redirect_uri': self.redirect_uri, + 'state': self.params.state + }) raise AuthorizeError( self.params.redirect_uri, 'server_error', diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 9c5f9dd..ef05ec4 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -1,3 +1,4 @@ +import logging import urllib from django.http import JsonResponse @@ -8,6 +9,7 @@ from oidc_provider.lib.utils.token import * from oidc_provider.models import * from oidc_provider import settings +logger = logging.getLogger(__name__) class TokenEndpoint(object): @@ -16,6 +18,11 @@ class TokenEndpoint(object): self.params = Params() self._extract_params() + logger.debug('Request %s', self.request) + logger.debug('TokenEndPoint request.POST --> : %s', self.request.POST) + logger.debug('TokenEndpoint request.GET --> : %s', self.request.GET) + logger.debug('TokenEndPoint extract_params --> : %s', self.params.__dict__) + def _extract_params(self): query_dict = self.request.POST @@ -29,21 +36,25 @@ class TokenEndpoint(object): def validate_params(self): if not (self.params.grant_type == 'authorization_code'): + logger.error('Unsupported grant type: --> : %s', self.params.grant_type) raise TokenError('unsupported_grant_type') try: self.client = Client.objects.get(client_id=self.params.client_id) if not (self.client.client_secret == self.params.client_secret): + logger.error('Invalid client, client secret -->: %s', self.params.client_secret) raise TokenError('invalid_client') if not (self.params.redirect_uri in self.client.redirect_uris): + logger.error('Invalid client, redirect_uri --> : %s', self.params.redirect_uri) raise TokenError('invalid_client') self.code = Code.objects.get(code=self.params.code) if not (self.code.client == self.client) \ or self.code.has_expired(): + logger.error('Invalid grant, code client --> %s', self.code.client) raise TokenError('invalid_grant') except Client.DoesNotExist: @@ -77,7 +88,7 @@ class TokenEndpoint(object): 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': id_token, } - + logger.debug('Response dictionary --> : %s', dic) return dic @classmethod @@ -89,4 +100,6 @@ class TokenEndpoint(object): response['Cache-Control'] = 'no-store' response['Pragma'] = 'no-cache' + logger.debug('JSON Response --> : %s', response.__dict__) + return response diff --git a/oidc_provider/tests/templates/accounts/login.html b/oidc_provider/tests/templates/accounts/login.html new file mode 100644 index 0000000..6c24774 --- /dev/null +++ b/oidc_provider/tests/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/oidc_provider/tests/templates/accounts/logout.html b/oidc_provider/tests/templates/accounts/logout.html new file mode 100644 index 0000000..25aa0f8 --- /dev/null +++ b/oidc_provider/tests/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/oidc_provider/tests/templates/base.html b/oidc_provider/tests/templates/base.html new file mode 100644 index 0000000..6d38b8b --- /dev/null +++ b/oidc_provider/tests/templates/base.html @@ -0,0 +1,50 @@ + + + + + + + OpenID Provider + + + + + + + + + + +
+
+ +

django-oidc-provider

+
+ + {% block content %}{% endblock %} + + + +
+ + + + + + + \ No newline at end of file diff --git a/oidc_provider/tests/templates/home.html b/oidc_provider/tests/templates/home.html new file mode 100644 index 0000000..c79d818 --- /dev/null +++ b/oidc_provider/tests/templates/home.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} + +
+

Welcome!

+

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.

+

View on Github

+
+ +{% endblock %} \ No newline at end of file diff --git a/oidc_provider/tests/templates/oidc_provider/authorize.html b/oidc_provider/tests/templates/oidc_provider/authorize.html new file mode 100644 index 0000000..ccc7065 --- /dev/null +++ b/oidc_provider/tests/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/oidc_provider/tests/templates/oidc_provider/error.html b/oidc_provider/tests/templates/oidc_provider/error.html new file mode 100644 index 0000000..b6e75dd --- /dev/null +++ b/oidc_provider/tests/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/oidc_provider/tests/test_settings.py b/oidc_provider/tests/test_settings.py new file mode 100644 index 0000000..0e158e1 --- /dev/null +++ b/oidc_provider/tests/test_settings.py @@ -0,0 +1,90 @@ +import os +from datetime import timedelta + +DEBUG = False + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +SITE_ID = 1 + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +) + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + + 'formatters': { + 'simple': { + 'format': '%(asctime)s %(process)d [%(levelname)s] %(name)s Line: %(lineno)s id: %(process)d : %(message)s' + } + }, + + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + } + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'filters': ['require_debug_false'], + 'formatter': 'simple', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler', + 'formatter': 'simple', + }, + "debug_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "DEBUG", + "formatter": "simple", + 'filename': 'debug.log', + 'formatter': 'simple', + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + } + }, + + 'loggers': { + 'oidc_provider': { + 'handlers': ['console', 'debug_file_handler'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'), + }, + }, +} + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.admin', + + 'oidc_provider', +) + +SECRET_KEY = 'secret-for-test-secret-top-secret' + +ROOT_URLCONF = 'oidc_provider.tests.test_urls' + +TEMPLATE_DIRS = ( + "oidc_provider/tests/templates", +) + +# OIDC Provider settings. + +SITE_URL = 'http://localhost:8000' \ No newline at end of file diff --git a/oidc_provider/tests/test_urls.py b/oidc_provider/tests/test_urls.py new file mode 100644 index 0000000..abfb2c4 --- /dev/null +++ b/oidc_provider/tests/test_urls.py @@ -0,0 +1,15 @@ +from django.contrib.auth import views as auth_views +from django.conf.urls import patterns, include, url +from django.contrib import admin +from django.views.generic import TemplateView + + +urlpatterns = patterns('', + url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'), + url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'), + url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'accounts/logout.html'}, name='logout'), + + url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), + + url(r'^admin/', include(admin.site.urls)), +) diff --git a/openid_provider/lib/utils/http.py b/openid_provider/lib/utils/http.py new file mode 100644 index 0000000..9b6a710 --- /dev/null +++ b/openid_provider/lib/utils/http.py @@ -0,0 +1,27 @@ +import json + +from django.core.serializers.json import DjangoJSONEncoder +from django.http import HttpResponse + + +# CLass JsonResponse see: https://github.com/django/django/blob/master/django/http/response.py#L456 + +class JsonResponse(HttpResponse): + """ + An HTTP response class that consumes data to be serialized to JSON. + :param data: Data to be dumped into json. By default only ``dict`` objects + are allowed to be passed due to a security flaw before EcmaScript 5. See + the ``safe`` parameter for more information. + :param encoder: Should be an json encoder class. Defaults to + ``django.core.serializers.json.DjangoJSONEncoder``. + :param safe: Controls if only ``dict`` objects may be serialized. Defaults + to ``True``. + """ + + +def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs): + if safe and not isinstance(data, dict): + raise TypeError('In order to allow non-dict objects to be serialized set the safe parameter to False') + kwargs.setdefault('content_type', 'application/json') + data = json.dumps(data, cls=encoder) + super(JsonResponse, self).__init__(content=data, **kwargs) diff --git a/openid_provider/settings.py b/openid_provider/settings.py new file mode 100644 index 0000000..26288b8 --- /dev/null +++ b/openid_provider/settings.py @@ -0,0 +1,83 @@ +from django.conf import settings + + +class DefaultSettings(object): + + @property + def LOGIN_URL(self): + """ + REQUIRED. + """ + return None + + @property + def SITE_URL(self): + """ + REQUIRED. + """ + 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): + """ + OPTIONAL. + """ + return 60*10 + + @property + def OIDC_EXTRA_SCOPE_CLAIMS(self): + """ + OPTIONAL. + """ + from oidc_provider.lib.claims import AbstractScopeClaims + + 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 + +default_settings = DefaultSettings() + +def get(name): + ''' + Helper function to use inside the package. + ''' + try: + value = getattr(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 diff --git a/setup.py b/setup.py index 142995c..93f7549 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,10 @@ setup( 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], + tests_require=[ + 'pyjwt==1.1.0' + ], + install_requires=[ 'pyjwt==1.1.0', ],